51 comments

[ 8.6 ms ] story [ 148 ms ] thread
TL;DR; ? would be better, but it would completely break our parser.

(The feature that conflicts, array literals, are using the obviously desirable syntax as well. This is just good intentions and path dependence in action.)

It's not like it's an implementation detail of their parser, it would make the language more difficult to write correctly and still be parsed by any parser, including a human reading the code.
100% agree, it just an unfortunate interaction between

1) ?: syntax 2) [] for dereferencing 3) [] for literals 4) ? for null chaining

Each of the decisions is correct in isolation, but together they’re a nightmare. Which decision you made first determines what you end up with in the other areas.

Exactly right.

If I had a time machine, (1) is what I'd fix. The conditional operator syntax has always been pretty hokey and if you just make `if` an expression (along with most other statement forms), you have no need for it.

Why not explicit

   Option.map (fun l -> Array.nth l n) your_optional_list
And why treat something nullable as a special entity when it's just a container?

And why would anybody want a nullable collection or a special syntax for 'em, when any collection is already nullable, and an empty list or array is basically the same thing as None/Null?

For Option types to work well and not be an awkward footgun (like C++ std::optional), you also need sum types and pattern matching.

While Dart should gain those, that's a much more invasive language change I reckon.

> For Option types to work well and not be an awkward footgun (like C++ std::optional), you also need sum types and pattern matching.

It works fine if you don't have these, though there are more tradeoffs (less convenience / compile-time safety).

C++'s std::optional is footguny because it is not a type-safety feature, it's an optimisation feature (a stack allocated pointer, hence it having "a pointer interface").

> For Option types to work well and not be an awkward footgun (like C++ std::optional), you also need sum types and pattern matching.

Which you can derive from lexical closures (which Dart has) using Church encoding:

    typedef Optional<T> = R Function<R>(R, R Function(T));
    
    Optional<T> None<T>() {
      return <R>(R none, R some(T x)) => none;
    }
    
    Optional<T> Some<T>(T x) {
      return <R>(R none, R some(T x)) => some(x);
    }
    
    Optional<B> MapOptional<A,B>(B f(A), Optional<A> option) {
      return <R>(R none, R some(B x)) => option(none, (a) => some(f(a)));
    }

    String OptionalString<T>(Optional<T> option) {
      return option('None()', (x) => 'Some(${x.toString()})');
    }
    
    void main() {
      Optional<num> opt1 = None();
      Optional<num> opt2 = Some(4);
      print('opt1: ${OptionalString(opt1)}');
      print('opt2: ${OptionalString(opt2)}');
      Optional<num> opt3 = MapOptional((x) => 2*x, opt2);
      print('2*opt2: ${OptionalString(opt3)}');
    }
Optional<T> is a sum type, and you pattern match on it by calling the function and providing expressions for the None and Some cases as parameters.

There may be some implementation limits... I managed to crash the compiler at https://dartpad.dartlang.org/ with the first version which had the expression for opt3 inlined into the call to OptionalString(). Declaring it as a separate variable worked around the issue. It's also apparently disallowed to nest generic function types, so you can't have Optional<Optional<T>>; this might be circumvented by wrapping the function type in a class.

Because most of these languages started from ubiquitous nullability and had to work from there. Also these safe dereference operators are not Option.map, they act as both Option.map and Option.flatMap "for convenience". Finally they tend to be method-oriented, which creates namespacing issues & is problematic when their null isn't a dedicated singleton (à la smalltalk / ruby / python).
I'm pretty sure Python's `None` is a singleton object
Yes? I'm mentioning it as an example of "singleton object" nulls. As opposed to non-object nulls like Java or JS's.
Ahh, whoops. I misread your comment. I thought you were saying the opposite
Yeah I can see where my wording would be unclear, sorry about that.
I was trying to find which programming language did it first, because I knew C# did it well before JS, although I definitely wouldn't be surprised if they weren't the first.

However, in my search I found a list of languages and how they use safe navigation operators:

https://en.wikipedia.org/wiki/Safe_navigation_operator

InterLisp allowed (car nil) and (cdr nil) to safely yield nil, thus making it safe to probe list structure to any depth. This spread into other dialects and is that way in Common Lisp

> In 1974, about a dozen persons attended a meeting at MIT between the MacLisp and Interlisp implementors, including Warren Teitelman, Alice Hartley, Jon L White, Jeff Golden, and Guy Steele. [ ... ] In the end only a trivial exchange of features resulted from “the great MacLisp/Interlisp summit”: MacLisp adopted from Interlisp the behavior (CAR NIL) → NIL and (CDR NIL) → NIL, and Interlisp adopted the concept of a read table.

[Evolution of Lisp, https://www.dreamsongs.com/Files/HOPL2-Uncut.pdf]

In the Common Lisp Object System, we can specialize any method to arguments of the null class, a type whose only element is nil. Thus for instance, if we have a generic function salary like this:

  (salary employee)
then if employee is nil, this simply calls for the following primary method to exist:

  (defmethod salary ((emp null))
    ...)
This can return whatever you want: just nil, or zero or whatever is appropriate.

If the method is not defined, then (salary nil) signals a condition.

> Finally, consider this example of chaining the subscript:

> someJson?[recipes]?[2]?[ingredients]?[pepper]

> To our eyes, that doesn’t look very good. It scans less like a method chain and more like some combination of infix operators — a little like ??. Compare it to this code:

> someJson?.[recipes]?.[2]?.[ingredients]?.[pepper]

Yep. So in the end ?[] does not look better, and the dot avoids the ambiguity with ? [] :

There you go. Dot. You could also allow `a.[x]` as a valid alternative to a[x]

Why exactly is it a problem when the intent here is to actually chain infix operators.
Because `?` is not an infix operator between two things of the same (or reasonably similar) type, which is what programmers expect from infix operator chains.
I see. Coming from swift the ?. syntax feels weird to me because it implies there is some property accessed or method called when there is not.

Ambiguity with ternary operator is unfortunate but this one is lifted (in Swift) by harder rules on whitespace. A postfix operator must not have space between the variable and the ?: operator must have space around.

> Coming from swift the ?. syntax feels weird to me because it implies there is some property accessed or method called when there is not.

There is. "[]" is a method in Dart. In Dart, almost all operators are syntactic sugar for corresponding method calls on the left-hand operand. In Dart, lists and maps are built into the core library, but the "[]" syntax is not special to those types. Any user-defined class is free to implement the subscript operator too.

In practice, this is useful because it means user-defined classes can implement the List and Map interfaces. This gives user collections the same ease of use as built-in ones.

(comment deleted)
It's so weird when you've been using a feature for years in a language (swift) and you see another language try to come up to solutions to a problem you never had..

Chaining ?[] sucks no matter what you do, and as for parser ambiguity, my guess is that if the parser doesn't know how to understand an expression, then the best thing is to have it fail and ask the programmer to add parenthesis. Because chances are it won't make any sense to a human as well.

> you see another language try to come up to solutions to a problem you never had

...didn't Swift have the same problem to witch it came with the same solution? (I mean `?.`)

  let a:[Int]? = nil
  let ai = a?[0]
and

  let b: [Int:[Int]]? = nil
  let bi = b?[0]?[0]

are valid in swift. ?. is used when you want to access a property, but i've never thought of it as an operator. Rather, i see it as "a?" (aka : unwrap a) followed by "." dot operator to access a property. I basically just replace a with a? every time a becomes nullable, and that's it.
"Chaining ?[] sucks no matter what you do"

I find myself wondering if making chaining easy just ends up affording bad design, and if that "suckage" is your program trying to tell you about your bad design. I don't chain very much at all, because I generally consider chaining to be a sign that I need to add a better method somewhere. The big exception being reaching into a "dead" data structure where there's no particular invariants being maintained and no real reason not to just reach in deeply, and, for most programs, you really shouldn't have very many of those. I'd say you almost certainly have a major design problem if you've got this chaining happening all the time.

(One such corner is that — — a and --a are both valid but mean different things.)

I'm not familiar with Dart but I believe the same applies to all the C-derived syntax languages; predecrement vs subtraction of negation. If I remember correctly, the C standard even has "a+++++b" as an example (which is a syntax error, since it parses as "a++ ++ +b", but "a++ + ++b" would not be.)

As a "completely uninformed outside" opinion, how about not adding another operator, but automatically choosing based on whether the type on the left is nullable? I feel like there's already a lot of "implicit nullness" going around with this sort of language design, such that these operators are redundancy overhead --- and according to the article, the declaration of the type already tells you whether it's nullable.

> I'm not familiar with Dart but I believe the same applies to all the C-derived syntax languages

Yes, you're correct. It's not a unique property of Dart, but it's an example of a case where the whitespace in your source code is important, even if you just use it as an input to the automated formatter.

> how about not adding another operator, but automatically choosing based on whether the type on the left is nullable?

We could do that, but that just kicks the problem down the road. It means the type of the surrounding expression would itself become nullable (because a null-safe subscript operator can always evaluate to null). So the user still has to handle it at some point. It's better to make them handle it at the first place that null can appear so that they know where it's sneaking into their code.

Anyone know what the bang operator does? It's impossible to Google (unlike "cascade syntax" a few lines down):

  Is similar to the syntax for the ! operator: e1![e2]
It's a non-null assertion operator which would throw an error if `e1 == null`.
Would the operator itself throw the error, or trying to access `[e2]` on null?

I never used Dart, I would assume it's basically a cast from `Optional<SomeType>` to `SomeType`, so wouldn't throw an exception itself

The operator itself would throw an error.

Dart is planning to have a sound non-nullability which means that if something has static type `SomeType` then it is guaranteed to be a value that is actually `SomeType` and not something else.

Thus you can't take `SomeType?` and just cast it to `SomeType` because `SomeType?` can be null and `SomeType` can't be null.

This means this cast has to perform a check and throw if your value is null.

This also means that if you have `e1` and `e1` has non-nullable static type then `e1` would never be null in the NNBD world - which is a very good property (e.g. for optimizations).

Interesting! So you were right to call it an assertion rather than a cast
I'll be uh... honest, I didn't realise anyone still used Dart.

Not meant to be a dig - just forgot it existed.

As to the topic - a single question mark seems to make more sense.

The Flutter framework is helping keep it alive. I think it's also used a lot in Google's Fuchsia OS.

I thought it was too bad it was not widely adopted as a JavaScript replacement, like it was meant to be.

Yes.

I have the feeling it occupied a weird place in the language ecosystem.

TypeScript was less ambitious and closer to JS so it got more momentum.

Dart is not as close to JS as TypeScript, but doesn't offer things like Reason while still being as far away from JS as Reason (Own language semantics, with own toolchain, etc.)

Flutter is very good. Therefore Dart.
Kotlin doesn't have the [] syntax. You just go someObjThatMayBeNull?.aMethod()?.aProperty : orSomeOtherExpression.

I think that wins in terms of readability. Also, every Android developer is already used to that syntax.

I hope Google at some point opens up Flutter to other languages. IMHO it's being held back by Dart at this point and they are barking up the wrong tree by not addressing that directly. People are reluctant to switch to it because they have to switch language to something less nice. The solution is not flogging the dead horse that is Dart by slowly adding bits of features that they hope will bridge the gap.

Kotlin now has a native compiler and reengineering Flutter to play nice with the LLVM ecosystem would also enable other languages. Like Swift, Rust or C++. Also, WASM is becoming a thing in the LLVM world. A Flutter evolution that properly integrated into that eco system would be quite a useful thing to have.

if Kotlin get to point where it can compile to both AOT and native, Flutter will adapt it.
[] is an indexed data operator for Lists, Maps and other collections. It isn't a general property access operator like in JavaScript. It makes it possible to have user-defined list implementations with the ergonomics of built-in arrays, and drastically increases readability over Java's get() and set() methods. Don't knock it.
Kotlin has [] but it can't be used with the null-safe navigation operator (?.)

If you want to index a datastructure that could be null, you use ?.get(...) instead of ?[...]

Not sure about Flutter being held back by Dart. From my experience the language works really well for Flutter.

For example Dart has first class support for asynchronous streams, very useful for a reactive framework. Flows in Kotlin are still experimental I think?

The Dart version of async/await works really well, just what you need in Flutter code and simpler than suspended functions in Kotlin.

The cascade operator is since syntactic sugar and, again, simpler and lighter than Kotlin's "apply".

Then there are Flutter's killer features, mainly hot reload and soon the Web support. Is Kotlin.js in the same league as Dart? Honest question! Dart's JavaScript support is very mature, with 2 backends: one for development and one for production. And what's the hot-reload story on Kotlin's side? I could not find an answer on Google, but I did find[1] these words from the "IntelliJ IDEA product manager @ JetBrains, co-author of Kotlin in Action":

"Flutter relies heavily on features that are only provided by the Dart VM. It’s not possible to add Kotlin support to Flutter without losing a lot of the things making Flutter attractive."

So from a marketing standpoint maybe Dart is a liability, but technically I think it's a strength. I hope marketing won't have the last word!

[1] https://twitter.com/intelliyole/status/1105493258954780672

> I hope marketing won't have the last word

It's interesting you think JVM support is just marketing.

For millions of developers around the globe, JVM support is a feature, and one they really can't do without.

Dart is a liability to them because unproven, and its future is equally uncertain given Google's track record.

> It's interesting you think JVM support is just marketing

I never said or implied that. I think maybe Dart is a liability from a marketing point of view because I saw many negative comments on HN which had more more to do with Dart's popularity than its technical merits. I was not talking about the JVM.

Every proven technology was unproven once. I remember when people said Java was too risky, GC was too slow, and it would never take off.
Which side are you arguing for ;) couldn’t resist
To be honest I feel like most people who knock on Dart have never actually written it.

   a?[b]
happens to be valid Rust already. Rust doesn't have the ternary `?:` operator, because almost everything is an expression, so you can use regular if/else anywhere. That ship has sailed for Dart, but the other syntax doesn't look too bad either.