Skip to content

if expressions #3374

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
subzero911 opened this issue Sep 29, 2023 · 26 comments
Open

if expressions #3374

subzero911 opened this issue Sep 29, 2023 · 26 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@subzero911
Copy link

subzero911 commented Sep 29, 2023

You already introduced switch expressions. Are you planning to add if expressions?

Syntax examples:

var x = if (a > 0) return 1; else return 2;
var x = if (a > 0) {
  return 1; 
} else if (b < 10) { 
  return 2;
} else {
  return 3;
}

Currently we have only x ? y : z operator, but it's not enough if we want more advanced branching.

@subzero911 subzero911 added the feature Proposed language feature that solves one or more problems label Sep 29, 2023
@mateusfccp
Copy link
Contributor

Does this solves your problem?

var x = switch ((a, b)) {
  (> 0, _) => 1,
  (_, < 10) => 2,
  _ => 3,
};

@subzero911
Copy link
Author

Does this solves your problem?

var x = switch ((a, b)) {
  (> 0, _) => 1,
  (_, < 10) => 2,
  _ => 3,
};

It looks very non-intuitive.

@subzero911
Copy link
Author

At least, there are if-expressions in Rust, Python, Kotlin and Swift, along with switch/match expressions:
https://doc.rust-lang.org/reference/expressions/if-expr.html
https://www.hackingwithswift.com/swift/5.9/if-switch-expressions
https://kotlinlang.org/docs/control-flow.html
https://note.nkmk.me/en/python-if-conditional-expressions/

@eernstg
Copy link
Member

eernstg commented Sep 29, 2023

@subzero911, it looks like you are asking for a way to write statements in an if expression. You could then use an immediately invoked function expression ("iife"):

var x = (){ if (a > 0) return 1; else return 2; }();

This would allow you to write arbitrary statements and deliver the result using return e;, because you are now simply using a normal if statement. (I don't know if this is what you mean by

Currently we have only x ? y : z operator, but it's not enough if we want more advanced branching.

because there's no branching that you can't express using the ?: operator and parentheses, but it is true that you can't write statements in a ?: expression.)

In any case, you might very well want to preserve the context type, because this can affect the meaning of a statement like return e;. You could then use a helper function, as described in this comment:

X iife<X>(X Function() f) => f();

double x = iife((){ if (a > 0) return 1; else return 2; });

This causes 1 and 2 to become "integer literals with static type double", which makes them work exactly like 1.0 and 2.0. If you use the form (){...}() then you won't have the context type double at each return statement, and the initialization of x will then be a compile-time error.

@lrhn
Copy link
Member

lrhn commented Sep 29, 2023

You can do everything with a conditional expression, ?/:, today that you would be able to do with if syntax, other than possilby not having an else branch. Which doesn't make sense for expressions, so likely not even that.

The original example can be written as:

var x =  (a > 0) ? 1 : (b < 10) ? 2 : 3;

Shorter. More like random line noise. But just as powerful.
There is no "more advanced branching" when all you do is binary branches.

Switches can do more advanced branching.

That said, I'd be happy to allow an if-expression (like we have an if-element), just for the more verbose syntax.
There is an issue for that, #2306

@subzero911
Copy link
Author

subzero911 commented Sep 29, 2023

var x =  (a > 0) ? 1 : (b < 10) ? 2 : 3;

This is a so-called "ternary hell", rather an example of "how you shouldn't do".
I mentioned a ternary operator just to let you know that I'm aware of it.

@Wdestroier
Copy link

@subzero911 Do you think var x = if (a > 0) 1 else if (b < 10) 2 else 3; is any better? Maybe we can solve the "ternary hell" by introducing the if-expression hell. I would rather have a > 0 ? 1 : (b < 10 ? 2 : 3).

@water-mizuu
Copy link

How about this?

var x = switch (null) {
  _ when a > 0 => 1,
  _ when b < 10 => 2,
  _ => 3,
};

@subzero911
Copy link
Author

@subzero911 Do you think var x = if (a > 0) 1 else if (b < 10) 2 else 3; is any better? Maybe we can solve the "ternary hell" by introducing the if-expression hell. I would rather have a > 0 ? 1 : (b < 10 ? 2 : 3).

Agreed, var x = if (a > 0) 1 else if (b < 10) 2 else 3; looks like a hell. Just add curly braces, and it would become readable (there's a lint suggesting to always use curly braces).

How about this?

var x = switch (null) {
  _ when a > 0 => 1,
  _ when b < 10 => 2,
  _ => 3,
};

Yeah, we have when clauses, I thought about it too.

@subzero911, it looks like you are asking for a way to write statements in an if expression. You could then use an immediately invoked function expression ("iife"):

var x = (){ if (a > 0) return 1; else return 2; }();

var x = if (a > 0) { return 1; } is still more concise than () {}() syntax.
Also, it's easy to miss the last () while reading.

@AlexanderFarkas
Copy link

AlexanderFarkas commented Oct 31, 2023

I personally don't like bringing every nice feature from other popular languages.

Reason why Rust/Kotlin/Python/Swift have such expressions - they were designed without ternary operator.
But if they had ternary operator for historical reasons, they wouldn't introduce a new way of doing the same thing with very little gain.

IIFE looks good to me - I use it all the time with switch expression, however I always end up with extracting them to separate functions - for readability and documentation purposes.

@AlexanderFarkas
Copy link

However, if Dart didn't choose to proceed with Pattern Matching and, instead, stuck to flow analysis, having if expression (instead of switch expressions) would be justifiable.

@caseycrogers
Copy link

caseycrogers commented Apr 15, 2024

May just be personal preference, but ternary expressions have been driving me absolutely mad. I desperately want an if...else expression so that I can completely avoid ternaries.

Here are my arguments in favor of an if...else expression:

  1. Ternary syntax scales horribly past a single predicate because each additional predicate requires another layer of nesting. With an if...else expression, no additional nesting is required so, with line breaks, even an arbitrarily long chain is quite readable. I'm not quite sure what the dartfmt enforced formatting should be, but here is one way it could look that is, IMO, highly readable and minimally nested:
return 
  if (predA) 'A'
  else if (predB) 'B'
  else if (predC) 'C'
  else null;
  1. Collection literals already allow us to use if...else like an expression in one specific context. Of course this is subtly different because exhaustivity is not required there, but there could simply be a static analysis error in the case where the dev did not provide a closing else to enforce exhaustivity. Allow if...else as an expression is more consistent with other parts of the language.
  2. Even if you have only a single predicate to check, the ternary syntax is really awkward. Namely, it flips the <keyword> <expression> syntax used everywhere else in dart, including in if statements:
if (<predicate) <expression> ...
(<predicate>) ? <expression> ... // <--- order is flipped, throws me off every time

My spicy-hot take is that ternaries are a dated syntax that doesn't make a lot of sense in a modern language.

Here are my thoughts on all the previous points made so far:

  1. IMO using a switch on a record or a switch with whens is verbose and is abusing the intent of a switch. Feels like it'd make it easy to introduce subtle bugs where the branching cases don't evaluate as the programmer intended.
  2. Supporting curly braces in if...else expressions would be nice, but is a whole other can of worms so it probably shouldn't be considered at least for now. See:
    hope for do expression #132
    Allow Multi-Statement Bodies in Switch Expressions #3117
  3. IIFE's feel like by far the best in-dart-today approach. They're a bit awkward though. They introduce some extra nesting and a lot of extra symbols. They also have unbounded complexity that open the door to the dev doing a lot of messy stuff inside of them. A basic chain of predicate checks is by far the most common scenario for me so I'd like the existing simple if...else syntax to cover that scenario rather than relying on a complicated escape hatch like IIFEs.

@ekuleshov
Copy link

There is a ternary expression x ? y : z and kind of unary null ??= guard. It would be also nice to have an unary conditional safeguard. expression.

Not sure about the syntax, but instead of something like this:

x = condition ? y : null

the thought is to avoid the "null" part and write something like:

x = if(condition) y

In a Flutter context it is commonly used for gesture callback, e.g. onTap, etc (convention a null callback makes widget disabled).

@lrhn
Copy link
Member

lrhn commented Apr 16, 2024

The example looks like it can just be

x = condition ? y : null;

or if assigning null is a no-op:

if (condition) x = y;

Saving the : null isn't that many characters (every reasonable syntax will have at least as many characters as
?/:), and it's makes the code less explicit. An implicit null isn't as readable as an explicit one.

It's also possible to implement this as an extension method.

extension GuardOrNull on bool {
  T? call<T>(T value) => this ? value : null;
}

Which you can use as:

  x = (condition)(x);

Biggest issue is that it doesn't promote.

I think a new syntax is more important for conditionally passing arguments, like

  foo(1, x: if (o != null) o.length)

so that book argument is passed if the condition is false.

@ekuleshov
Copy link

ekuleshov commented Apr 16, 2024

The example looks like it can just be

x = condition ? y : null;

or if assigning null is a no-op:

if (condition) x = y;

Saving the : null isn't that many characters (every reasonable syntax will have at least as many characters as ?/:), and it's makes the code less explicit. An implicit null isn't as readable as an explicit one.

In your last assessment you don't take into account readability when y expression is long (sometimes several lines long).

And your example with an if statement can't be used when you need to return a value. Eg. Flutter reference in my comment:

onTap: if(buttonIsEnabled && notTheLastEntry) _onDelete,

Instead of extension on a boolean we could use a static method to be used like:

onTap: ifTrue(buttonIsEnabled && notTheLastEntry, _onDelete),

Though with an inline function declaration that will require an additional lambda, comparing with ternary or potential unary conditional expression.

@harryfei

This comment was marked as off-topic.

@domhel
Copy link

domhel commented Sep 15, 2024

Actually, there is an advantage of Rust's if expression compared to the ternary operator.

let foo = if bar < 10 {
  let x = 6;
  let y = 7;
  x + y
} else {
  bar
}

The nice part here is that we can use additional variables inside the if scope, while still avoiding a mutable variable.
In Dart, however, we can only do this with mutable variables:

int foo = bar;
if (bar < 10) {
  final x = 6;
  final y = 7;
  foo = x + y;
}

There is no way to have foo being final without making the ternary operator ugly or introducing variables in the most outer scope.

I miss this feature from Rust and I often find myself in these situations.

But this is probably one of those problems that one can only have if they have experienced the other language.

@mateusfccp
Copy link
Contributor

@domhel You can make foo final using statement if and introducing variables in the local scope of the statement.

final int foo;

if (bar < 10) {
  final x = 6;
  final y = 7;
  foo = x + y;
} else {
  foo = bar;
}

It's slightly more verbose, but not impossible (and definitely more readable than using ternary).

@ekuleshov
Copy link

Readability is a subjective thing. But statements can't be used everywhere. E.g can't use them when building collections.

@mateusfccp
Copy link
Contributor

Readability is a subjective thing. But statements can't be used everywhere. E.g can't use them when building collections.

Sure, I didn't say I'm against if expressions, I just informed him that for his specific case, it was not needed.

@subzero911
Copy link
Author

Readability is a subjective thing. But statements can't be used everywhere. E.g can't use them when building collections.

We have "collection if" and "collection for" in Dart.

@munificent
Copy link
Member

I think it's unlikely that we'd add if expressions unless we went all the way and made everything an expression, which is what Rust, Scala, and Kotlin do. Otherwise, I think the benefit is too marginal over ?: if the only thing you can have inside each branch is just a single expression.

However, I do think we should allow a switch expression or statement to omit the scrutinee (#3457). If we did that, then, similar to what @water-mizuu suggests, you could do:

var x = switch {
  when a > 0 => 1,
  when b < 10 => 2,
  => 3,
};

That looks pretty nice to me.

@caseycrogers
Copy link

caseycrogers commented Dec 16, 2024

I ran into a new pain with ternaries: they don't (as far as I can tell) support pattern matching:

// Syntax error.
final foo = (maybeFoo case final nonNullFoo?) ? processFoo(nonNullFoo)  :  Foo.defaultInstance;

Could we at the very least get single-line if...else expressions? These would be more or less analogous to if...else in a collection with the one exception that they're required to be exhaustive (have an else clause):

// Each clause must be a single line expression, no curly braces allowed.
// Syntax error if `else` is not provided.
// Also allows an arbitrary number of `else if` clauses and supports pattern matching.
final foo =  if (foo case final nonNullFoo?) processFoo(nonNullFoo)  else  Foo.defaultInstance;

This would allow us to replace every use of ternary syntax with an if...else which would be a big win (at least for ternary-haters such as myself). It's FAR more readable and consistent with the rest of the language..

@lrhn
Copy link
Member

lrhn commented Dec 17, 2024

Correct, conditional expressions do not support case conditions. Switch expressions do, so you can write:

final foo = switch (foo) { final nonNullFoo? => processFoo(nonNullFoo), _ => Foo.defaultInstance};

The formatter won't allow you to keep it on one line, though. The conditional expression might just be short enough to be allowed. Maybe.

(Also, pet-peeve: The ?/: operator is the conditional operator, or just a "conditional expression". Calling it "the ternary operator" is misleading since Dart has two ternary operators, the other being []=. <old man tilting at windmills image>)

@yakagami
Copy link

yakagami commented Mar 6, 2025

Here's an example from flutter dart:ui where I would have liked this: https://github.com/flutter/flutter/blob/c45b835577b01777e70681c7b751af82784ff3a4/engine/src/flutter/lib/ui/math.dart#L13

double clampDouble(double x, double min, double max) {
  assert(min <= max && !max.isNaN && !min.isNaN);
  if (x < min) {
    return min;
  }
  if (x > max) {
    return max;
  }
  if (x.isNaN) {
    return max;
  }
  return x;
}

I only used this once and wanted to write it myself so I could remove the dart:ui import. (Also didn't need the asserts or NaN handling.) Would be nice to be able to do

a = 
  x > max ? max:
  x < min ? min: 
  x;

if the formatter supported that. I do also like this though:

a = switch {
   when x > max -> max,
   when x < min -> min,
   _ -> x
}

(non-constant expressions would need switch-when and plain switch would still be constant only, right?)

However, I think Kotlin when looks better because it is shorter:

a = when {
   x > max -> max,
   x < min -> min,
   _ -> x
}

I also think

a = if {
   x > max -> max,
   x < min -> min,
   else -> x,
}

looks good, which is similar to #3457 (comment) except as an expression.

@munificent
Copy link
Member

if the formatter supported that. I do also like this though:

a = switch {
   when x > max -> max,
   when x < min -> min,
   _ -> x
}

That's essentially #3457. I agree it would be nice.

(non-constant expressions would need switch-when and plain switch would still be constant only, right?)

That's correct.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests