language
language copied to clipboard
Remove/redefine conditional expression syntax.
The current "ternary" conditional expression syntax, e1 ? e2 : e3
, uses the ?
character which also have several other uses in the Dart syntax (null-aware member access, nullable spread operators, nullable types, all null-related).
It would be nice to use ?
for more null aware syntax, but it very easily conflicts with the ?
/:
syntax.
So, let's remove that syntax.
Since we still want expression-level conditionals, we can embrace what we are already doing in literals, and what we are suggesting doing with switch
: Make the statement syntax also work as expression syntax.
Proposal
That is, change e1 ? e2 : e3
to if (e1) e2 else e3
as an expression. (Disclaimer: Cannot be used to start a statement expression or map/set literal element. Cannot omit else
, or alternatively, can only omit else
where null
is a valid value, with a default of else null
. The else
binds to nearest possible if
in case of ambiguity. The usual.)
Grammar
The grammar is simple, we put it where the ?
/:
expression currently is:
<conditionalExpression> ::= <ifNullExpression> |
`if' `(' <expression> `') <expressionWithoutCascade> (`else' <expressionWithoutCascade>)?
with the provision that
The expression of a <statementExpression> cannot be a <conditionalExpression> starting with
if
. An <expressionElement> expression cannot be a <conditionalExpression> starting withif
.
Semantics
Same as ?
/:
, with the extra option of omitting the else
branch, in which case it's implicitly the same as else null
.
Expressions always have a value.
Migration
We can automatically migrate all e1 ? e2 : e3
to if (e1) e2 else e3
. We will have to do so.
Pros
Not many by itself
-
Some people will likely find the
if
syntax easier to read than the?
/:
. Especially in contexts where?
and:
both mean something else as well. -
Allows omitting
else
. I have written far too many expression of the formx != null ? action(x) : null
. Usingif (x != null) action(x)
is actually four characters shorter than the?
/:
. -
Opens up the grammar to using
?
more for other null-aware operations! This is the goal!
Cons
-
More verbose. The
?
/:
syntax is literally two characters, it's very hard to make shorter. Theif
syntax is eight characters ("if()else"). You save some whitespace in the beginning (if (e1) e2
vse1 ? e2
-
The same
if
syntax now exists in three different versions (expressions, elements and statements). That's consistent, but also makes it harder to recognize what's going on without further scrutiny. -
Allows forgetting
else
. That gives you a nullable value, which you may notice if the then expression wasn't nullable. Accidental mistakes can be hidden if the then branch was already nullable. -
Requires migration, even if it's automated.
Why?
Opening up the ?
to things.
Consider null-aware operators: x ?< 4
, x ?- 2
.
Without the conditional expression in the mix, parsing this should be "more doable".
I do occasionally see multiply-nested conditional expressions, which will get a few characters wider with this syntax. But there's an opportunity with the migration to upgrade to a case expression where applicable:
void foo(MyEnum a) {
return a == MyEnum.one
? "one"
: a == MyEnum.two
? "two"
: a == MyEnum.three
? "three"
: "four";
}
// becomes
void foo(MyEnum a) {
return switch (a) {
case MyEnum.one => "one";
case MyEnum.two => "two";
case MyEnum.three => "three";
case MyEnum.four => "four";
};
}
Would this allow for nullableFunction?(arg1, arg2)
instead of nullableFunction?.call(arg1, arg2)
?
This has potential to make nested conditional expressions much more legible.
Take this code for example, which was formatted using dart format
:
String grade(int scoreInPercent) => scoreInPercent >= 90
? 'A'
: scoreInPercent >= 80
? 'B'
: scoreInPercent >= 70
? 'C'
: scoreInPercent >= 60
? 'D'
: 'F';
With the if
/else
syntax it would probably be formatted like this:
String grade(int scoreInPercent) =>
if (scoreInPercent >= 90)
'A'
else if (scoreInPercent >= 80)
'B'
else if (scoreInPercent >= 70)
'C'
else if (scoreInPercent >= 60)
'D'
else
'F';
Collection-if puts us in a potentially confusing position. If we do if
expressions and always require them to have an else
clause then there is little room for trouble since at that point an if
element and an if
expression behave the same when the clauses are both expressions. But if we allowing omitting the else
, the they behave differently:
var y = if (false) 1; // "null".
print([if (false) 1]); // "[]", *not* "[null]".
Now imagine that we do what we've wanted for several years and allow if
for arguments:
arg([int? x = 1]) print(x);
arg(if (false) 2);
Does this print 1
or null
?
Before we go in this direction, I think we should have a very clear model of when a value is absent versus when a value is filled in with null
. And we should verify that users actually intuit that model.
A potentially larger problem is that if we give them if
expressions, they may reasonably want to have block bodies for the branches and then we need block expressions and at that point we're basically into making everything an expression. I'm in favor of that in principle, but it would be a big change for the language that we'd have to do carefully. I'm not aware of any statement-based languages that have changed to being expression-oriented after the fact.
I generally agree that defaulting to null
is dangerous. With null safety, it's easier to detect if it happens by accident, but it's still possible to just forget an else
.
I think I'd prefer to require the else
in expression context.
In a collection element context, or maybe even an optional argument context, we can allow omitting the else
branch, and then it really should mean "no value". (Unlike in a collection, it doesn't mean "no entry" in a parameter list, just no value for the specific optional argument.) Those will not really be expression if
s then.
Forgive me if I'm wrong, would this issue pave the way for something like this to be implemented?
final result = if (foo) {
final value = _someMethod();
value + 4
} else {
final value = _someMethod();
value + 2
}
Forgive me if I'm wrong, would this issue pave the way for something like this to be implemented?
final result = if (foo) { final value = _someMethod(); value + 4 } else { final value = _someMethod(); value + 2 }
I don't think so, but the new switch might do what you want.
I love this proposal. I absolutely hate writing ?/:
ternaries with a fiery passion.
Why?
- I'm much more used to writing
if (<condition>) <A> else <B>
. The ternary syntax flips the keyword and the condition which throws me off every single time - Having an
...if else...
equivalent in the ternary syntax requires nesting which I find to basically never be viable because of just how bad the readability is.if (<cond1>) else if (<cond2>)...
is way more readable and intuitive.
// Unreadable nightmare and cringe.
return someBool ? 1 : someOtherBool ? 2 : 3;
// Much more readable. Based.
return if (someBool) 1 else if (someOtherBool) 2 else 3;
- The
if
syntax is used in statements, switch+conditional guards, and probably more places I'm not thinking of. This makes the ternary syntax stick out like a sore thumb.
I'm not a fan of the suggested implicit null. It feels like it has a lot of potential footguns. I'd be for requiring exhaustivity or an implicit void
. But I'm trying to think of examples where the implicit null could be helpful. I think transforming nullable variables:
final String? foo = getFoo();
// Is null if foo was null and/or parsing fails.
bar = if (foo != null) int.tryParse(foo);
If we imagine this proposal going through, it could also lay groundwork for a much more dramatic longterm shift towards everything-is-an-expression with multi-statement if-expression blocks which interplays nicely with multi statement switch bodies: https://github.com/dart-lang/language/issues/3117#issuecomment-1613823889 If you find you need to add a side effect to your single statement if expression it wouldn't require an annoying refactor:
// Before.
var foo = if (someBool) 1 : 2;
// After.
var foo = if (someBool) {
someSideEffect();
1;
} else {
2;
}
A couple more changes and you'd have what I secretly wanted this whole time: Dart is just Scala without the baggage.
All this kind of begs the question: why not just use a switch expression? And I'd argue that switch expressions have a very specific purpose: they're a restrictive construction designed to give the compiler enough information to barf if you haven't directly addressed each potential case.
You can theoretically coerce any arbitrary if
chain into a switch expression, but for most of them you're probably not going to get compile errors when the input space changes (eg an enum gets a new value) which was the whole point of a switch. IMO using them in contexts where you've undermined the compiler's ability to do this for you (eg including a default
clause) is self-defeating and giving you an illusion of safety. It should be avoided.