language icon indicating copy to clipboard operation
language copied to clipboard

Remove/redefine conditional expression syntax.

Open lrhn opened this issue 2 years ago • 5 comments

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 with if.

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 form x != null ? action(x) : null. Using if (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. The if syntax is eight characters ("if()else"). You save some whitespace in the beginning (if (e1) e2 vs e1 ? 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".

lrhn avatar Jun 23 '22 13:06 lrhn

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";
  };
}

srawlins avatar Jun 23 '22 17:06 srawlins

Would this allow for nullableFunction?(arg1, arg2) instead of nullableFunction?.call(arg1, arg2) ?

mmcdon20 avatar Jun 23 '22 18:06 mmcdon20

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';

mmcdon20 avatar Jun 24 '22 01:06 mmcdon20

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.

munificent avatar Jun 28 '22 17:06 munificent

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 ifs then.

lrhn avatar Jun 28 '22 18:06 lrhn

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
}

Reprevise avatar Jan 26 '23 19:01 Reprevise

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.

FMorschel avatar Apr 27 '23 13:04 FMorschel

I love this proposal. I absolutely hate writing ?/: ternaries with a fiery passion.

Why?

  1. 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
  2. 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;
  1. 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.

caseycrogers avatar Nov 08 '23 18:11 caseycrogers