Skip to content

Allow switch without scrutinee and patterns in cases #3457

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
alexcmgit opened this issue Nov 9, 2023 · 15 comments
Open

Allow switch without scrutinee and patterns in cases #3457

alexcmgit opened this issue Nov 9, 2023 · 15 comments
Labels
patterns Issues related to pattern matching. request Requests to resolve a particular developer problem

Comments

@alexcmgit
Copy link

Recently Dart 3 introduced switch expressions, although they facilitate type-casting-like flows through pattern matching, we still can't use flows where there is no compile-time pattern to match but a runtime expression to evaluate:

late final action;

if (isAvailableX()) {
  action = doAndReturnX();
} else if (isAvailableY()) {
  action = doAndReturnY();
} else if (isAvailableZ()) {
  action = doAndReturnZ();
}

Using switch expression we are forced to give the switch the runtime object we want to work with along with the compile-time patterns:

final action = switch (null) {
  _ when isAvailableX() => doAndReturnX(),
  _ when isAvailableY() => doAndReturnY(),
  _ when isAvailableZ() => doAndReturnZ(),
  _ => null,
};

In Kotlin we can simply:

val action = when {
  isAvailableX() -> doAndReturnX()
  isAvailableY() -> doAndReturnY()
  isAvailableZ() -> doAndReturnZ()
  else -> null
}

A similar approach in Dart would be:

final action = switch {
  isAvailableX() => doAndReturnX(),
  isAvailableY() => doAndReturnY(),
  isAvailableZ() => doAndReturnZ(),
  _ => null,
};
@alexcmgit alexcmgit added the request Requests to resolve a particular developer problem label Nov 9, 2023
@lrhn lrhn added the patterns Issues related to pattern matching. label Nov 9, 2023
@lrhn
Copy link
Member

lrhn commented Nov 9, 2023

So if a switch has no expression, the patterns are all reduced to the when clause expressions (no value matches no pattern 100% of the time), or to a _ or default pattern.

This would apply to both expression and statment switches:

int compare(int a, int b) => switch {
  a < b => -1,   // no 'pattern when'
  a == b => 0,
  _ => 1,
};

and

int compare(int a, int b) {
  switch {
    case a < b: return -1;   // no 'pattern when'
    case a == b: return 0;
    default: return 1;
  }
}

Not unreasonable. (Can we remove if now? 😉)

@munificent
Copy link
Member

I love it.

@lrhn
Copy link
Member

lrhn commented Nov 10, 2023

Thinking about it more. Maybe this should be an if, not a switch:

if {
  a < b => -1,
  a == b => 0,
  else => 1,
}

Or maybe not, leave if to the binary choice, and always use switch for multi-way choices. That's consistent, and the syntax is built for it. I didn't try to make an if statement syntax.
(We could allow else as a default branch of switch expressions, I think it reads quite well.)

It would also pretty much assume an expression-if, but when you can do

switch (cond) {
  true => thenExpr,
   _ => elseExpr,
 }

today, that's not a big leap.

@alexcmgit
Copy link
Author

I particularly like the if syntax, it sounds natural and readable as when:

  • "when condition B meets, do B".
  • "if condition B meets, do B".

when: We almost got there using switch + when, so we can abbreviate to when and make it an actual language keyword and not just a guard clause.

if: I think it's no longer just a binary choice mechanism since Dart 3 when if case syntax was released. So I see no problems in using the switch + when with if { } syntax).

Also, I agree that else is more readable... actually I wish it was allowed on switch expressions. It's far more readable than putting a _.

@munificent munificent changed the title Make switch object and patterns optional Allow switch without scrutinee and patterns in cases Mar 8, 2024
@lrhn
Copy link
Member

lrhn commented Apr 14, 2024

This is about choosing the first of a list of possible cases whose boolean condition evaluates to true.
That really is an if/else chain. Maybe if the formatter allowed a briefer syntax, we wouldn't need to introduce another one:

if (isAvailableX()) action = doAndReturnX();
else if (isAvailableY()) action = doAndReturnY();
else if (isAvailableZ()) action = doAndReturnZ();

or even

if (isAvailableX()) action = doAndReturnX(); 
else 
if (isAvailableY()) action = doAndReturnY(); 
else 
if (isAvailableZ()) action = doAndReturnZ();

These are all valid Dart syntaxes, just not ones supported by the formatter.

If feel like a bunch of the syntax requests we get are for things that wouldn't be necessary if the formatter was less strict about putting things on the same line.
If the formatter allowed:

  void oneLineFunction(arg) { expressionStatement; }

we wouldn't have needed to special-case

  void oneLineFunction(arg) => expressionStatement;

and if we allowed if (test) expressionStatement; to stay on one line, even if followed by an else, then maybe we wouldn't need this issue.

That said, I can see why:

if {
  isAvailableX(): action = doAndReturnX();
  isAvailableY(): action = doAndReturnY();
  isAvailableZ(): action = doAndReturnZ();
}

might read even better. The else is really noise here.
Maybe we need a shorter else if?

if (isAvailableX()) action = doAndReturnX(); 
 | (isAvailableY()) action = doAndReturnY(); 
 | (isAvailableZ()) action = doAndReturnZ();

(I know that'l never parse, but it does look nice.)

@alexcmgit
Copy link
Author

This is about choosing the first of a list of possible cases whose boolean condition evaluates to true.

I do agree.

That really is an if/else chain.

Ifs aren't allowed to be used within an expression context as switch does. You cannot:

final value = if (isAvailableA()) getA() 
              else if (isAvailableB()) getB() 
              else null;

Even if it did, we are still left with the verbose else issue.

and if we allowed if (test) expressionStatement; to stay on one line, even if followed by an else, then maybe we wouldn't need this issue.

This issue is not just about having a clever way of "choosing the first of a list of possible cases whose boolean condition evaluates to true" but also being able to do that within an expression context.


Let's say we have the following syntaxes:

if (isAvailableX()) action = doAndReturnX();
else if (isAvailableY()) action = doAndReturnY();
else if (isAvailableZ()) action = doAndReturnZ();
if (isAvailableX()) action = doAndReturnX(); 
else 
if (isAvailableY()) action = doAndReturnY(); 
else 
if (isAvailableZ()) action = doAndReturnZ();
if {
  isAvailableX(): action = doAndReturnX();
  isAvailableY(): action = doAndReturnY();
  isAvailableZ(): action = doAndReturnZ();
}
if (isAvailableX()) action = doAndReturnX(); 
 | (isAvailableY()) action = doAndReturnY(); 
 | (isAvailableZ()) action = doAndReturnZ();

All of them need to write action = over and over again.

The key is to be able to do a clever if/else within an expression context (e.g one-line function, variable assignments).

@ghost
Copy link

ghost commented Apr 15, 2024

There's an option to reuse the syntax of conditional expression by adding some decorations, with a different formatting

var x = if {
  : cond1 ? expr
  : cond2 ? expr
  : expr
}

This syntax works and formats nicely with or without the assignment. (The first colon is added for beauty)
Sadly, it dissonates with switch syntax.

@lrhn
Copy link
Member

lrhn commented Apr 15, 2024

Don't need the if for that:

action = 
  isAvailableX() ? doAndReturnX() :
  isAvailableY() ? doAndReturnY() :
  isAvailableZ() ? doAndReturnZ() :
  null;

We already have the syntax for condition chains as expressions. The biggest issue is the formatter not recognizing a chain of theses as something that should be laid out at the same indentation level.

@ghost
Copy link

ghost commented Apr 15, 2024

Suppose the formatter somehow acquires the ability to nicely format this expression. This will solve the problem of if- expressions syntax, right?
Then your example of if- statement

if {
  isAvailableX(): action = doAndReturnX();
  isAvailableY(): action = doAndReturnY();
  isAvailableZ(): action = doAndReturnZ();
}

can be encoded like this:

isAvailableX() ? action = doAndReturnX() :
isAvailableY() ? action = doAndReturnY() :
isAvailableZ() ? action = doAndReturnZ() :
doNothing; // we can use null here, too

The problem is that the syntax forces us to write an extra line which has no meaning. An obvious solution is to make the last colon optional (in this context), so we can write it simply as

isAvailableX() ? action = doAndReturnX() :
isAvailableY() ? action = doAndReturnY() :
isAvailableZ() ? action = doAndReturnZ() ;

Does this solve the problem? Or it will need some extra decor - e.g. in the form of enclosing if {...} for readability?
If we do need it, then wouldn't it be more consistent to apply the same decor to the chain of if-expressions, too?

@lrhn
Copy link
Member

lrhn commented Apr 15, 2024

My worry here is that we may be chasing brevity, without actually achieving readability.

The examples here show things that are similar. They have similar structures because they do similar things in similar ways.

The goal of code formatting is to expose such similarities instead of hiding them.
Automatic formatting might not always be able to achieve that, because it cannot know which similarities are fundamental, and which are accidental. That is, sometimes automatic formatting doesn't give the most readable result.

Adding new syntax to get around that seems like treating a self-inflicted wound. We could just allow code to be exempted from automatic formatting, and then the author can do whatever brings out the similarities, even if it's as un-traditional as:

if (isAvailableX()) action = doAndReturnX(); 
else 
if (isAvailableY()) action = doAndReturnY(); 
else 
if (isAvailableZ()) action = doAndReturnZ();

Maybe there will be a handful of non-standard patterns that people end up preferring for specific code patterns, and maybe the formatter can eventually learn to recognize and support them.

But we shouldn't introduce new syntax just to get around the formatter. It's much easier to change or ignore the formatter then.

@alexcmgit
Copy link
Author

alexcmgit commented Apr 15, 2024

Changing the formatter to allow something like this:

final value = 
    isAvailableA() ? getA() :
    isAvailableB() ? getB() : 
    isAvailableC() ? getC() : null;

is going to solve the issue of not having a clever way of "choosing the first of a list of possible cases whose boolean condition evaluates to true".

But I think the best output of this issue would be to have a more unified flow control for multiple choices, like we have in Kotlin with the when keyword.

What about integrating the runtime evaluation with all current compile-time supported features of the switch patterns? So, for instance, I would be able to create patterns as we do now, and if none of them pass the test, I could check for something at runtime.

TestLevel level;

// Scrutinee optional. So `switch { ... }` is also valid.
final result = switch (level) {
  // Currently throws 'The binary operator is is not supported as a constant pattern.' (it's a non-constant expression)
  // Only supported through pattern-matching `FirstSpecialCaseLevel()` (Less readable but possible to resolve as constant).
  is FirstSpecialCaseLevel => ...,

  // Currently throws 'The relational pattern expression must be a constant.'
  // Also, If no scrutinee is given, throws a compile-error, since there are no value to compare `< minimumAllowed()` with.
  < minimumAllowed() => ...,
  > maximumAllowed() => ...,
  tooHigh() => ...,
  tooLow() => ...,
  withinHealthLevels() => ...,

  is SpecialCaseLevel => ...,
  AnotherSpecialCaseLevel(:final field) when ... => ...,
  anotherCheck() when lastCheck() => ...,
  _ => ...,
};

I wonder if this may have implications because we will be mixing different kinds of expressions ("non-constant" and "constant") within the same "scope" (I've no internal SDK knowledge so feel free to correct misplaced terms).

@ghost
Copy link

ghost commented Apr 16, 2024

I think we are chasing not brevity for its own sake, but rather trying to come up with visually recognizable patterns, and those tend to be relatively short.

Imagine that isAvailableX() and others are not there, and instead we have to write them as inline expressions, each of different size. Then good formatting would be a challenge even for an author.
The way out is to introduce intermediate variables.

late isAvailableX = ...long expression
late isAvailableY = ...
late isAvailableZ = ...

The goal is to make every cond ? expr : fit in a single line.
Can someone provide an example where the introduction of auxiliary variables and/or functions makes the program less readable?

@alexcmgit
Copy link
Author

Currently the only way I can do that is by:

final isAvailableX = (() {
  // ... long expression with ifs and explicit returns
})();

// instead of
final isAvailableX = // ... a bunch of mixed '&&', '||' and '()' with unrelated subjects.

Although it would be a great improvement, I think it's outside of the scope of the current issue (at least from what I thought initially, but I would love to see these inline expressions as well).

final isAvailableX => {
  if (check1) return result1;
  if (check2) return result2;
  return resultDefault;
};

@ghost
Copy link

ghost commented Apr 16, 2024

Assuming you don't really need a function , you can write the condition as

final isAvailableX  =
   check1 ? result1 :
   check2 ? result2 :
   resultDefault;

If you do need a function, you can write it in a similar manner:

isAvailableX()  =>
   check1 ? result1 :
   check2 ? result2 :
   resultDefault;

The chain of conditional expressions is an exact equivalent to a switch without the scrutinee (the topic of current thread)

@ghost
Copy link

ghost commented Mar 23, 2025

It's clear by that point that this kind of formatting for a conditional operator won't be possible in general (at least, not automatically)

isAvailableX()  =>
   check1 ? result1 :
   check2 ? result2 :
   resultDefault;

But I found more than a few cases where "switch without scrutinee" would make the program cleaner. This request belongs to the category of "small and useful features". It's very small. And very useful.

isAvailableX()  {
  return switch {
    check1 => result1,
    check2 => result2,
    _ => resultDefault,
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
patterns Issues related to pattern matching. request Requests to resolve a particular developer problem
Projects
None yet
Development

No branches or pull requests

3 participants