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.
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.
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.
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").
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 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:
Probably lisp, if we aren't counting syntax, since most standard or auxilary toolbelt libraries have had something similar since the age of programming languages:
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.
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:
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.
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.
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.
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.
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).
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.)
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.
[] 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.
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!
> 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.
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.
51 comments
[ 8.6 ms ] story [ 148 ms ] thread(The feature that conflicts, array literals, are using the obviously desirable syntax as well. This is just good intentions and path dependence in action.)
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.
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.
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?
While Dart should gain those, that's a much more invasive language change I reckon.
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").
Which you can derive from lexical closures (which Dart has) using Church encoding:
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.
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
https://www.eiffel.org/doc/eiffel/Void-safety-_Background%2C...
https://lodash.com/docs/4.17.15#get
> 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:
then if employee is nil, this simply calls for the following primary method to exist: 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.
> 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]
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.
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.
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.
...didn't Swift have the same problem to witch it came with the same solution? (I mean `?.`)
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.
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.
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.
I never used Dart, I would assume it's basically a cast from `Optional<SomeType>` to `SomeType`, so wouldn't throw an exception itself
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).
Not meant to be a dig - just forgot it existed.
As to the topic - a single question mark seems to make more sense.
I thought it was too bad it was not widely adopted as a JavaScript replacement, like it was meant to be.
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.)
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 you want to index a datastructure that could be null, you use ?.get(...) instead of ?[...]
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
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.
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.