-
Notifications
You must be signed in to change notification settings - Fork 205
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
Case expressions and return patterns #4141
Comments
also, in the grammar for the |
I find some of the // Case expressions of type `bool`.
// var b1 = 10 case > 0; // OK, sets `b1` to true.
var b1 = 10 > 0;
// var b2 = 10 case int j when j < 0; // OK, sets `b2` to false.
var b2 = 10 > 0;
// Case expressions of other types.
// var s = "Hello, world!" case String(length: > 5) && return.substring(0, 5);
var h = "Hello, world!"; // Might be nice to have let for this example
var s = (h.length > 5) ? h.substring(0, 5) : null
// var n = [1, 2.5] case [int(isEven: true) && return, _] || [_, double() && < 3.0 && return];
var n = switch ([1, 2.5]) {
[int(isEven: true) && var i, _] => i,
[_, double() && < 3.0 && var d] => d,
_ => null
};
Object? value = (null, 3);
// var i = value case (num? _, int return j) when j.isOdd;
var i = switch (value) { (num? _, int j) when j.isOdd => j, _ => null} |
@leafpetersen if we are talking purely about if we are talking about the |
Would you mind sharing some motivational examples? It's useful to see examples of how folks could imagine using a feature. |
@leafpetersen First of all, i want to admit, that when i was looking for examples for the I remember wanting it, when i was writing parser combinator, but since it was only a hobby project, i didn't backup the code (*), and now i can't find it. There are other project where i wanted to use it (at least in some capacity), like recursive descent parser and DFA to regex transpiler, but i also didn't backup the projects. But before you write off this feature, here are the use-cases i found:
final list = [...];
final filtered = list.where((e) => e case Foo(:int prop) || Bar(:int prop) when prop > 5);
// or
final filtered = list.where((e) {
return e case Foo(:int prop) || Bar(:int prop) when prop > 5;
});
// example 1 - when i was doing recursive descent parser
// nextTok -> Token?
while(nextTok case Token(type: .string || .number, :final value)){
// can use value, and other fields we destructure
...
}
// example 2
// parsePrimary() -> PrimaryNode?
while(parsePrimary() case var node?){
}
// without it - example 1
while(true){
if(nextTok == null || ![TokenType.string, TokenType.number].contains(nextTok.type)) break;
final value = nextTok.value;
}
// without it - example 2
while(true){
final node = parsePrimary();
if(node == null) break;
} In essence, after thinking about it, i think the use-cases for both are niche, but are also quite good and don't cause many problems. In the first case, the destructured variables would be scoped to the These are all the uses i found, so you can decide if you wish to have it, or not. Either way, i won't complain whether it's implemented or not. *i hate overusing git for short projects |
@hydro63 thanks! I think the The I'd suggest giving those issues a thumbs up since I think they will likely remain the canonical issues for those specific sub-features. |
Are going to support this, too? var x = a > b && return a || return b; |
@tatumizer i suppose not. Disregarding the missing Edit - i thought the comment was referencing this issue, but it was instead referencing another proposal. |
Here are some examples where case expressions are useful, based on the document Dart Patterns -- Survival of the fittest: // Currently in Flutter code.
String? runtimeBuildForSelectedXcode;
final Object? iosDetails = decodeResult[iosKey];
if (iosDetails != null && iosDetails is Map<String, Object?>) {
final Object? preferredBuild = iosDetails['preferredBuild'];
if (preferredBuild is String) {
runtimeBuildForSelectedXcode = preferredBuild;
}
}
// Using current patterns.
final String? runtimeBuildForSelectedXcode = switch (decodeResult[iosKey]) {
{'preferredBuild': final String preferredBuild} => preferredBuild,
_ => null,
};
// Using a case expression.
final String? runtimeBuildForSelectedXcode =
decodeResult[iosKey] case {'preferredBuild': String return}; The main abbreviation above comes from the fact that a case expression that contains a return pattern evaluates to null when the match fails. However, that does seem to be a useful behavior to get implicitly. Note that this semantics is similar to the semantics of expressions like It could be argued (for expressions using null shorting like // Currently in Flutter code.
T? get currentState {
final Element? element = _currentElement;
if (element is StatefulElement) {
final StatefulElement statefulElement = element;
final State state = statefulElement.state;
if (state is T) {
return state;
}
}
return null;
}
// We could already simplify this using pre-existing Dart features.
T? get currentState {
final Element? element = _currentElement;
if (element is StatefulElement) {
final State state = element.state;
if (state is T) return state;
}
return null;
}
// Using current patterns.
T? get currentState => switch (_currentElement) {
StatefulElement(:final T state) => state,
_ => null,
};
// Using a case expression.
T? get currentState => _currentElement case StatefulElement(state: T return); The most interesting part above is probably the type tests: The case expression evaluates to null if // Currently in Flutter code.
class ParsedProjectGroup {
ParsedProjectGroup._(this.identifier, this.children, this.name);
factory ParsedProjectGroup.fromJson(String key, Map<String, Object?> data) {
String? name;
if (data['name'] is String) {
name = data['name']! as String;
} else if (data['path'] is String) {
name = data['path']! as String;
}
final List<String> parsedChildren = <String>[];
if (data['children'] is List<Object?>) {
for (final Object? item in data['children']! as List<Object?>) {
if (item is String) {
parsedChildren.add(item);
}
}
return ParsedProjectGroup._(key, parsedChildren, name);
}
return ParsedProjectGroup._(key, null, name);
}
final String identifier;
final List<String>? children;
final String? name;
}
// Using current patterns.
class ParsedProjectGroup {
ParsedProjectGroup.fromJson(this.identifier, Map<String, Object?> data)
: children = switch (data['children']) {
final List<Object?> children => children.whereType<String>().toList(),
_ => null,
},
name = switch (data) {
{'name': final String name} => name,
{'path': final String path} => path,
_ => null,
};
final String identifier;
final List<String>? children;
final String? name;
}
// Using a case expression.
class ParsedProjectGroup {
ParsedProjectGroup.fromJson(this.identifier, Map<String, Object?> data)
: children = data['children'] case List return.whereType<String>().toList(),
name = data case {'name': String return} || {'path': String return};
final String identifier;
final List<String>? children;
final String? name;
} In the example above it's worth noting that // Currently in Flutter code.
int? findIndexByKey(Key key) {
if (findChildIndexCallback == null) {
return null;
}
final Key childKey;
if (key is _SaltedValueKey) {
childKey = key.value;
} else {
childKey = key;
}
return findChildIndexCallback!(childKey);
}
// Using current patterns.
int? findIndexByKey(Key key) {
late final Key childKey = switch (key) {
_SaltedValueKey(:final Key value) => value,
final Key key => key,
};
return findChildIndexCallback?.call(childKey);
}
// Using a case expression.
int? findIndexByKey(Key key) => findChildIndexCallback?.call(
key case _SaltedValueKey(value: Key return) || return
); In the example above we again get rid of some repetition of names. Note that we still avoid evaluating the key when // Currently in Flutter code.
Diagnosticable? _exceptionToDiagnosticable() {
final Object exception = this.exception;
if (exception is FlutterError) {
return exception;
}
if (exception is AssertionError && exception.message is FlutterError) {
return exception.message! as FlutterError;
}
return null;
}
// Using current patterns.
Diagnosticable? _exceptionToDiagnosticable() => switch (exception) {
AssertionError(message: final FlutterError error) => error,
final FlutterError error => error,
_ => null,
};
// Using a case expression.
Diagnosticable? _exceptionToDiagnosticable() => exception case
AssertionError(message: FlutterError return) || FlutterError return; This example once again uses a case expression to obtain a value with a choice: If we're looking at an // Currently in Flutter code.
final bool hasError = decoration.errorText != null;
final bool isFocused = Focus.of(context).hasFocus;
InputBorder? resolveInputBorder() {
if (hasError) {
if (isFocused) {
return decoration.focusedErrorBorder;
}
return decoration.errorBorder;
}
if (isFocused) {
return decoration.focusedBorder;
}
if (decoration.enabled) {
return decoration.enabledBorder;
}
return decoration.border;
}
// Using current patterns.
final bool isFocused = Focus.of(context).hasFocus;
InputBorder? resolveInputBorder() => switch (decoration) {
InputDecoration(errorText: _?) when isFocused => decoration.focusedErrorBorder,
InputDecoration(errorText: _?) => decoration.errorBorder,
InputDecoration() when isFocused => decoration.focusedBorder,
InputDecoration(enabled: true) => decoration.enabledBorder,
InputDecoration() => decoration.border,
};
// Alternative solution using current patterns.
InputBorder? resolveInputBorder() => switch ((
enabled: decoration.enabled,
focused: Focus.of(context).hasFocus,
error: decoration.errorText != null,
)) {
(enabled: _, focused: true, error: true) => decoration.focusedErrorBorder,
(enabled: _, focused: _, error: true) => decoration.errorBorder,
(enabled: _, focused: true, error: _) => decoration.focusedBorder,
(enabled: true, focused: _, error: _) => decoration.enabledBorder,
(enabled: false, focused: _, error: _) => decoration.border,
};
// Using a case expression (and assuming #4124).
final bool isFocused = Focus.of(context).hasFocus;
InputBorder? resolveInputBorder() => isFocused
? decoration case _(errorText: _?) && return.focusedErrorBorder ||
return.focusedBorder
: decoration case _(enabled: true) && return.enabledBorder ||
return.border; In this last example we're inspecting the given |
@hydro63 wrote:
It allows a case expression to occur in a location where an void main {
var x = MyClass()
..property1 = e1 case P1
..property2 = e2 case P2;
} It's probably a good trade-off to accept the slightly more complex grammar in return for being able to use case expressions in places like this. Otherwise we would just have to put
The special exception about case expressions in However, it should certainly be possible to allow a while loop to have a case expression as its condition, with the same special treatment of the scope, and similarly for a few other constructs (probably not do-while because it would be really weird to have declarations at the end of a block where they are in scope). In all other locations we can use a case expression as an expression, but the variables that it declares are not in scope anywhere other than in the @leafpetersen wrote:
Indeed, the examples in the original posting were only intended to illustrate the semantics. A set of more meaningful examples can be found here. @tatumizer wrote:
I don't quite know how this would generalize, but However, we can use a case expression to avoid declaring some variables (and writing those names twice): abstract class A { int get a; }
abstract class B { double get b; }
void foo(Object o) {
// Using a case expression.
var x = o case A(a: return) || B(b: return); // `x` has type `num?`.
// Using a switch expression.
var y = switch (o) {
A(:final a) => a,
B(:final b) => b,
_ => null,
};
} We can also use it to perform a bit of processing on the objects that we've found via pattern matching: abstract class A { double get a; }
abstract class B { String get b; }
void foo(Object o) {
var x = (o case A(a: return.toInt()) || B(b: return.length)) ?? 42; // `ab` has type `int`.
} |
@eernstg: The question closely related to this: consider void foo(Object o) {
var x = o case A(a: return) || B(b: return);
// VS
o case A(a: return) || B(b: return);
} Is the second expression allowed? |
Right, that's probably #2025. That would be a separate proposal, and it wouldn't conflict with this one (in this proposal The different forms of return pattern do not give rise to a returning completion of the current expression evaluation or statement execution, they just specify the value of the current pattern during a pattern matching process. void foo() {
var x = 1 case return;
print('Still here!'); // Yes, this will be printed, and `x` has the value 1.
}
Exactly. And the parser will have no difficulties knowing that void foo(Object o) {
var x = o case A(a: return) || B(b: return); // OK.
// VS
o case A(a: return) || B(b: return); // OK.
} Both case expressions will investigate the object In line 1 of the body of |
The compiler can certainly parse the same word differently in different contexts, but the problem is that the user has to parse it in the head, which may lead to confusion. There's nothing magical in the Unrelated: I think you are overdoing it in |
I've wanted case-expressions for a long time. Wanted them in a The Something like ((var tmp = e) case P1) ? v1 :
(tmp case P2) ? v2 : null It's not totally clear that Otherwise, the best way I have found to think about the The The pattern (I'm fine with making |
FWIW, among the examples given above in #4141 (comment) I find the ones marked as "using current pattern syntax" most readable. With "return patterns", the notation is shorter, but you have to pay for brevity by an extra effort of re-scanning the text back and forth in an attempt to parse it into a more human-friendly representation, which (arguably) is close to the "using current syntax" variant. An interesting experiment in literary form though 👍 😄 |
My biggest problem with The |
@tatumizer wrote:
I do agree that we have to tread carefully when we introduce a new language construct using existing special words (reserved words, built-in identifiers, or even regular identifiers like However, we have lots of situations where these special words have different meanings in different contexts, and so do other terms. For example: The For patterns, in particular, we have the following distinction: In an irrefutable context, a plain identifier that occurs as a pattern denotes a new local variable which is being introduced into the current scope (like void main() {
const c = 1;
var (x, y) = (c, c + 1);
switch (x) {
case c: print('Match!');
}
} In other words, it's definitely not a new thing that a reader of code needs to take the context into account in order to recognize the meaning of a special word or even an apparently very simple expression. I think the same thing will work for return patterns: When
The idea is that a case expression We could certainly restrict return patterns such that they can only allow us to say that the value of the case expression is the currently matched object. However, I expect the ability to compute a value based on that matched object to be quite useful. Currently I'm just proposing that a return pattern admits a chain of selectors, which will work without issues in the grammar. More general expressions are probably possible, but I suspect that they will create difficulties for the parser, and possibly also for a human reader of the code. That may or may not be true. In any case, we can explore generalizations in that direction if it seems to be useful and manageable. In some cases you can just move the selector chain out of the case expression, but if you wish to treat different matched values differently then it won't work so easily: class A {}
class B1 extends A {
List<int> get b1 => [42];
}
class B2 extends A {
double get b2 => 4.2;
}
void foo(A a) {
// Use return patterns in the same way everywhere.
var s = a case B1(b1: return.toString()) || B2(b2: return.toString());
// Not using it: In this case we can simply move the selector chain out.
var s2 = (a case B1(b1: return) || B2(b2: return)).toString();
// Using return patterns with different selector chains.
var i = a case B1(b1: return.length) || B2(b2: return.toInt());
// Not using it: Requires a full-blown switch expression.
var i2 = switch (a) {
B1(:final b1) => b1.length,
B2(:final b2) => b2.toInt(),
_ => null,
};
} The reasons why I haven't proposed that the return pattern should specify a general expression (like
Note that the ability to have a selector chain does not introduce extra syntax in the basic case where we just use a plain Of course, extension methods already allow us to turn an arbitrary expression into a getter or method invocation, that is, into a selector chain: extension<X> on X {
Y doCall<Y>(Y Function(X) callback) => callback(this);
}
void foo(int i) => i case return.doCall((self) => 17 ~/ self); I don't think this would be a commonly used technique, but it might be useful if we're repeatedly encountering situations where we'd like to use a return pattern with a more general kind of expression than a selector chain. |
@lrhn wrote:
Sounds like case expressions whose pattern does not contain any return patterns. Very good, that should just work out of the box for I'm not so sure about We agree completely about the scoping, that variables introduced by the pattern are in scope in the body of the
Surprise, surprise. ;-)
A return pattern only specifies the value of the nearest enclosing case expression, it does not have any control flow effects. In particular, we don't stop matching the pattern of a case expression just because a return pattern was matched successfully, instead we remember the matched value (and the selector chain, if any), and continue to match the rest of the pattern. It is enforced by static analysis that the pattern matching step will never find multiple matched values for any pattern containing one or more return patterns. (This is very similar to the analysis that makes it a compile-time error to use the same variable name too few or to many times in a pattern: For class A {
final bool b;
final int i, j;
A(this.b, this.i, this.j);
}
extension on int {
int effect() {
print('Working on $this!');
return this + 10;
}
}
void main() {
var x = A(false, 1, 2) case A(i: return.effect(), b: true) || A(b: false, j: return.effect());
} This will invoke
Surely they have a different semantics. I'd just recommend not thinking in terms of JavaScript when trying to read Dart code.
This is an abbreviation mechanism, obviously you can express the same thing using existing syntax (but you may have to do some upper bound computations on types manually in order to be able to rewrite a case expression as a somewhat longer switch expression). For void main() {
// Original expression.
var x1 = e case (P1 && return) || (P2 && return);
// Alternative ways to do the same thing.
var x2 = e case (P1 || P2) && return; // Seems natural.
var x3 = switch (e) { // A bit longer.
(P1 || P2) && var tmp => tmp,
_ => null,
};
final tmp = e;
var x4 = tmp case P1 || P2 ? tmp : null; // Needs the separate `tmp` declaration.
SomeSuitableType? x5; // Initially null.
if (e case (P1 || P2) && final tmp) x5 = tmp;
}
Certainly not. I'd much prefer to maintain the semi-declarative nature of patterns. That is, patterns look declarative, and they mostly work in a declarative manner because the invoked getters generally do not have observable side effects. The true semantics is, of course, that every getter invocation (or operator invocation, etc) that a pattern matching process can perform can have arbitrary side effects, and (in general, in principle) we always need to be aware of the evaluation order of every single subpattern of any pattern, and their potential side-effects. However, that isn't going to get significantly worse if we add return patterns.
Yes, you can, but it's a compile-time error if
That sounds right! (except that it doesn't short-circuit anything)
It is a pattern because the proposal specifies that it is a pattern. ;-) Just like The return pattern Of course, it would be possible to specify the mechanism such that it will evaluate the selector chain of every return pattern which is matched, but I think it's more useful to only evaluate the selector chain in the case where we have actually succeeded in a complete match of the top-level pattern The purpose of this selector chain is to proceed with further computations that are needed by the receiver of the value of the case expression. The reason why it's convenient to be able to specify the selector chain on each return pattern (rather than using I do agree that the computations performed in the selector chain are not about pattern matching (so they can't be expected to be essentially side-effect free, unlike the pattern matching itself). They are about post-processing of the matched value for a given purpose.
Sure, but it's hardly realistic to require that any given language mechanism must be unable to express the same semantics as some other construct (already in the language, or proposed).
I'd like that, too! But an expression of the form |
@hydro63 wrote:
I hope this helps: For a plain var x1 = [1, 2, 3] case [_, return, ...]; // `x1 == 2`.
var x2 = [1, 2, 3] case [_, ...return]; // `x2 == [2, 3]`.
var x3 = [1, 2, 3] case List<int>(length: > 2) && return; // `x3 = [1, 2, 3]`. For a For a return pattern with a selector chain, e.g., |
var x = A(false, 1, 2) case A(i: return.effect(), b: true) when i > 15; This still looks like an experiment in form - James Joyce kind of thing: you have to scan the sentence back and forth and translate it to a simpler representation in your head. This kind of unorthodox prose may appeal to some readers, but in Dart, this style would feel slightly out of place IMO. I don't question the originality of the idea, and appreciate it, but developing fluency in reading programs written in this style would require a long painstaking practice IMO. 😄 (Also of note is that there's not one, not two, but several syntax ideas here that don't rhyme with anything in dart) |
I'm not sure it makes sense for a pattern with a The I'd prefer to do something like var x = [1, 2, 3] case [_, return, ...]; as something like: var x = let [_, result, ...] = [1, 2, 3] in result; ... or just like we can do today: var [_, x, ...] = [1, 2, 3]; That keeps patterns for themselves and expressions for them selves, and doesn't try to make patterns have a value, except when they don't. |
var x = A(false, 1, 2) case A(i: return.effect(), b: true) when i > 15; Let's try. We can skip Next, the keyword For the pattern itself, I'd like to promote readings that are focused on the declarative nature of the pattern syntax. It looks like we're describing a situation, in a way that doesn't involve side effects or evaluation orders, we're just looking at the situation as a motionless picture. Of course, the actual detailed semantics is that there is a very specific evaluation order, and side effects could make the whole thing behave in completely different ways if you reorder a couple of subpatterns. But I'd claim that it is bad style to write code where evaluation order and side effects play an observable role for a pattern matching operation. Imperative semantics should be expressed using imperative style coding. The style guide already helps, because getter invocations aren't supposed to have observable side effects. The example above was written specifically in order to focus on the details of the evaluation order, because this is about language design, and the detailed semantics must obviously be well-defined. So I wrote an example where the elements are written in a somewhat quirky and surprising order, simply because I wanted to make the ordering of side effects noticeable, and in order to cover some not-so-obvious cases. In particular, I'd expect the above to be written as A return pattern with a selector chain does play a separate role in the semantics. At the end, when we have checked that everything matches the given pattern, the return pattern The fact that this happens at the very end is unusual, but I also think it's the most comprehensible timing: Nobody promises that a selector chain won't have side effects, and we definitely don't want var x = switch (A(false, 1, 2)) {
A(i: final result, b: true) when i > 15 => result.effect(),
_ => null
}; In summary, I do recognize that the detailed evaluation order and handling of side effects must be well defined, but I do not think it's good style to write code where the evaluation order and side effects matter during pattern matching (with or without return patterns, that is!). The only thing to be aware of is that a return pattern (including the ones with a selector chain) is executed at the very end, when a pattern match has been successfully achieved. That should be more reader-friendly than 'scan the sentence back and forth'. |
@lrhn wrote:
A return pattern does not prevent pattern matching from proceeding, and variables would be bound if and only if the same variables would be bound if the return pattern were replaced by a wildcard pattern. Hence, the
I'm sure that would be a very natural way to use this feature.
Sure, if the case expression occurs as the initializing expression of a local variable then we can just edit the whole declaration and make it a However, that won't work if the case expression occurs in any other context. For instance, if the variable isn't local, or if the case expression isn't used to initialize a variable at all, but is passed as an actual argument to a method invocation. Another crucial difference is that the pattern in the case expression is in a refutable position whereas the pattern in the pattern variable declaration (and presumably in the For case expressions, the fact that we're exploring a situation and may not have it is at the core of the feature. That's the reason why a direct and faithful rewrite of a case expression uses a switch expression: var x = switch ([1, 2, 3]) {
[_, final result, ...] => result,
_ => null,
}; |
But not necessary. Got it! That's an easier model to reason about. If anything, I'd just treat Still not sure the feature is worth it. It's just a shorthand for It's also not general enough to, fx, extract two values. I think I'd rather just tell people to do: (pair case ([..., var first], [..., var second])) ? (first, second) : null Explicit over implicit, for the If we wanted to add add anything for that, it's an if (pair case ([..., var first], [..., var second])) (first, second)` (Or not, that doesn't really read well.) |
Exactly!
That's true. My assumption is that it is really tempting to allow void main() {
var x1 = e case Something(a: true, b: < 10, return.foo()) || OtherThing(:final c, d: return.bar(c));
var x2 = switch (e) {
Something(a: true, b: < 10, final result) => result.foo(),
OtherThing(:final c, :final d) => d.bar(c)),
};
} It's like "I have it right here, let me use it right here!" vs. "OK, we need to use this matched value later on, let's put a label on it; later: use the label". |
Here's a modest idea: introduce a no-frills // using current syntax
T? get currentState => switch (_currentElement) {
StatefulElement(:final T state) => state,
_ => null,
};
// Using a case expression as proposed by OP
T? get currentState => _currentElement case StatefulElement(state: T return);
// Using case? expression: no new syntax except `?`
T? get currentState => _currentElement case? StatefulElement(:final T state) => state; (It can be considered a special case of |
A |
A I do agree that we could make this proposal a bit less radical by dropping the return patterns and having For example: var x1 = e case
Something(a: true, b: < 10, return.foo()) ||
OtherThing(:final c, d: return.bar(c)); If we didn't have return patterns then we'd have to split this into two cases (so the var x1 = switch (e) {
Something(a: true, b: < 10, final result) => result.foo(),
OtherThing(:final c, d: final result) => result.bar(c),
}; We can't use a single case because there is no reason to expect that the two variables named The semantics of these two expressions is exactly the same (including evaluation order), but with the switch expression we have to invent a name for the result (in each case) and then we have to refer to it after the |
I do like the switch (m) { p when c => e } or you can do (m case p when c => e) Grammar migth be a problem. The (m case p when (x) => y) A function literal will never be a A But maybe the (m case p when c) ? e : null for the same effect, which is only 4-5 characters longer than |
The problem is that the variables defined in pattern p must be available in But if we support |
I'm pretty sure we would want to have a special rule about conditional expressions ( That's also what I've chosen for the proposal in the OP of this issue. |
For this, you have to disallow shadowing (which is what java does), otherwise
will be a valid expression, where the meaning of |
I don't think this situation is worse than other kinds of shadowing, and we don't prevent that: var x = 0;
int y;
if (foo case Foo(:final x)) {
y = x+1; // `x` from pattern.
} else {
y = x-1; // `x` from enclosing scope.
} It might be helpful to lint this kind of situation, but that would then apply to all these situations. dart-lang/linter#872 didn't fly, but similar lints could be considered. |
It didn't fly because it was too broad and too restrictive. But sure, if dart today allows shadowing in patterns, and has no plans of disallowing it, then what you are suggesting is not worse. var x = a case Foo(:final x) when x>0 ? x+1 : null; You can even handle several alternatives: var x =
(a case Foo(:final x) when x>0 ? x+1 : null) ??
(a case Bar(:final y) when y.isEven ? y+1: null); Agree? 😄 |
@lrhn: an extra argument in favor of var a = switch {
cond1 => v1,
cond2 => v2
}; would often require an extra line If The advantage of |
I generalized the proposal to include an optional I also simplified the return patterns such that they can't introduce variables, and there is only one form (previously the proposal had void main() {
// Today.
var x1 = switch (expression) {
Sometype sometype => sometype.oneGetter,
OtherType otherType => otherType.otherGetter,
_ => null,
};
// With return patterns.
var x2 = expression case
Sometype return.oneGetter ||
OtherType return.otherGetter;
// Of course, we could also use return patterns in switch expressions.
// This could be useful, e.g., if `Sometype` and `OtherType` are the
// immediate subtypes of a sealed type (no need for a default case,
// and `x3` could have a non-nullable type).
var x3 = switch (expression) {
Sometype return.oneGetter,
OtherType return.otherGetter,
}
} |
I think a more straightforward solution would be to allow introducing local variables with short names in some contexts where it's not yet allowed. If the names are short (e.g. 1-letter), they can serve as convenient "pronouns" var s = "Hello, world!" case String(length: > 5) && return.substring(0, 5);
// VS
var s = "Hello, world!" case String(length: > 5, this: var e) => e.substring(0, 5); Today, AFAIK, we can introduce short names for the fields inside the pattern, but not for the "scrutinee". var n = [1, 2.5] case [int(isEven: true) && return, _] || [_, double() && < 3.0 && return];
// VS
var n = switch? ([1, 2.5]) {
[int(isEven: true, this: var i), _] => i,
[_, double(this: var d) ] when d < 3.0 => d;
}; (It would be good to find some syntax shorter than |
I still don't like how You can't do About the var n = switch? ([1, 2.5]) {
[int(isEven: true) && var i, _] => i,
[_, double d && < 3.0] => d;
}; That's shorter than the Or as a conditional expression, either: ([1, 2.5] case [int(isEven: true) && num n, _] || [_, double() && < 3.0 && num n])? n : null or ([1,2.5] case var p) &&
((p case [int(isEven: true) && var i, _]) ? i :
(p case[_, double d] when d < 3.0) ? d :
null) ( That is, Everything else discussed here, the |
Dart 3.0 introduced patterns, including the following kind of construct known as an if-case statement:
This statement has a semantics that makes it different from traditional if statements. In particular, it isn't specified as an if-statement whose condition is the expression
json case [int x, int y]
. The reason for this is that there is no expression of the form<expression> 'case' <guardedPattern>
. Hence, an if-case statement does not have a condition expression of typebool
, it has an expression (of an any type), and it has a pattern, and it chooses the "then" branch if and only if the pattern matches the value of that expression. Another specialty with if-case statements is that variable declarations in the pattern are in scope in that "then" branch.This issue claims that we might as well turn the syntax
<expression> 'case' <guardedPattern>
into an expression in its own right. We're generalizing it a bit, and allowing it to occur in any place where we can have an expression. The new kind of expression is known as a<caseExpression>
. For starters, it has static typebool
, and it evaluates to true if and only if the match succeeds.We say that the result type of the pattern is
bool
, which is true for all the patterns that we can write today.This issue also proposes a new kind of pattern,
<returnPattern>
, which is used to specify a different value for the evaluation of the enclosing case expression when it matches, namely the matched value. In this case, the result type of the pattern isM?
when the matched value type at the return pattern isM
. The value of the pattern when the match fails is null.Here are some examples illustrating the semantics (some less trivial examples can be seen in this comment):
Proposal
Syntax
Static Analysis
With this feature, every pattern has a result type. The result type of every pattern which is expressible without this feature is
bool
.The remaining patterns (which are only expressible when this feature is available) have a result type which is determined by the return patterns that occur in the pattern. They are specified below in terms of simpler patterns followed by composite ones.
The result type of a return pattern of the form
return
is the matched value type of the pattern. The result type of a return pattern of the formT return
isT
. The result type of a return pattern of the formreturn s1 .. sk
wheresj
is derived from<selector>
is the static type of an expression of the formv s1 .. sk
, wherev
is a fresh variable whose type is the matched value type of the pattern. Finally, the result type of a return pattern of the formT return s1 .. sk
is the static type of an expression of the formv s1 .. sk
, wherev
is a fresh variable whose type isT
.For example, if the matched value type for a given return pattern
P
of the formreturn.substring(5).length
isString
then the result type ofP
isint
. This is becausev.substring(5).length
has typeint
whenv
is assumed to have typeString
.The result type of an object pattern that contains one field pattern with result type
R
, which is notbool
, isR
. It is a compile-time error if the object pattern has two or more field patterns with a result type that isn'tbool
.It is a compile-time error if a listPattern, mapPattern, or recordPattern contains multiple elements (list elements, value patterns of the map, or pattern fields of the record) whose result type is different from
bool
. If all elements have result typebool
then the result type of the pattern isbool
. Otherwise, exactly one element has a result typeT
which is notbool
, and the result type of the pattern is thenT
.Consider a logicalAndPattern
P
of the formP1 && P2 .. && Pn
wherePj
has result typeT
which is notbool
, andPi
has result typebool
for alli != j
. The result type ofP
isT
. It is a compile-time error if a logicalAndPatternP
of the formP1 && P2 .. && Pn
has two or more operandsPi
andPj
(wherei != j
) whose result type is notbool
.Consider a logicalOrPattern
P
of the formP1 || P2 .. || Pn
wherePi
has result typeTi
, fori
in1 .. n
. A compile-time error occurs if at least one operandPj
has result typebool
, and at least one operandPk
has a result typeT
which is notbool
. If all operands have result typebool
then the result type ofP
isbool
. Otherwise, the result type ofP
is the standard upper bound of the result typesT1 .. Tn
.Consider a parenthesizedPattern
P
of the form(P1)
. The result type ofP
is the result type ofP1
.Some other kinds of pattern also have the result type of their lone child: castPattern, nullCheckPattern, and nullAssertPattern.
The remaining patterns always have result type
bool
: constantPattern, variablePattern, and identifierPattern.Assume that
e
is a case expression of the forme1 case P
whereP
has result typeT
which is notbool
; the static type ofe
is thenT?
. Assume thatP
has result typebool
; the static type ofe
is thenbool
.Assume that
e
is a case expression of the forme1 case P => e2
. A compile-time error occurs if the result type ofP
is notbool
. The static type ofe
is thenT?
, whereT
is the static type ofe2
.With pre-feature patterns, it is an error if a pattern of the form
P1 || .. || Pn
declares different sets of variables in different operandsPi
andPj
, withi
andj
in1 .. n
andi != j
. This is no longer an error, but it is an error to access a variable from outsidePi
orPj
unless it is declared by every operandP1 ... Pn
, with the same type and finality.The point is that we may well want to access different variables in return patterns:
e case A(:final x, y: return.foo(x)) || B(:final a, b: return.bar.baz(a + 1))
. For instance, awhen
clause which is shared among several patterns connected by||
cannot use a variable likex
, but the return patternreturn.foo(x)
can use it.Variables which are declared by a pattern in a case expression are in the current scope of the pattern itself, and in the current scope of the guard expression, if any.
Moreover, such variables are in scope in the first branch of an
if
statement whose condition is a case expression (just like an if-case statement today), and in the first branch of a conditional expression (that is,(e case A(:final int b)) ? b : 42
is allowed). Finally, such variables are in scope in the body of awhile
statement whose condition is a case expression.Dynamic Semantics
Evaluation of a case expression
e case P
whereP
has result typebool
proceeds as follows:e
is evaluated to an objecto
, andP
is matched againsto
. If the match succeeds then the case expression evaluates to true, otherwise it evaluates to false.Evaluation of a case expression
e case P
whereP
has result typeT
which is notbool
proceeds as follows:e
is evaluated to an objecto
, andP
is matched againsto
, yielding an objectr
. Ifr
is null then the case expression evaluates to null. Otherwise,r
is a function, and the case expression then evaluates tor()
.A return pattern of the form
T return
evaluates to() => v
wherev
is a fresh variable whose value is the matched value when the matched value has typeT
, otherwise it evaluates to null. A return pattern of the formreturn
evaluates to() => v
wherev
is a fresh variable whose value is the matched value (and it never fails to match).A return pattern of the form
return s1 s2 .. sk
wheresj
is derived from<selector>
evaluates to the value() => v s1 s2 .. sk
wherev
is a fresh variable bound to the matched value. A return pattern of the formT return s1 .. sk
wheresj
is derived from<selector>
evaluates to the value () => v s1 .. skwhere
vis a fresh variable bound to the matched value if the matched value has type
T. If the matched value does not have type
T` then it evaluates to null.For example,
return.foo()
evaluates to() => v.foo()
wherev
is the matched value.An object pattern that contains one field pattern with result type
R
, which is notbool
, is evaluated by performing the type test specified by the object pattern on the matched value, yielding null if it fails, and otherwise evaluating each of the pattern fields in textual order. If every pattern field yields true, except one which yields a non-null objectr
then the object pattern evaluates tor
. Otherwise the object pattern evaluates to null.A listPattern, mapPattern, or recordPattern is evaluated in the corresponding manner, yielding the non-null object
r
from the element whose result type is notbool
when all other elements yield true, and yielding null if any element yields false or null.Consider a logicalAndPattern
P
of the formP1 && P2 .. && Pn
wherePj
has result typeT
which is notbool
, andPi
has result typebool
for alli != j
.P
is evaluated by evaluatingP1
, ...,Pn
in that order. If every result is either true (when the result type isbool
) or a non-null objectr
(when the result type is notbool
), the evaluation ofP
yieldsr
. Otherwise it yields null.Consider a logicalOrPattern
P
of the formP1 || P2 .. || Pn
wherePi
has result typeTi
, fori
in1 .. n
. The case whereTi == bool
for alli
has the same semantics as today. Hence, we can assume thatTi != bool
for everyi
. Evaluation ofP
proceeds by evaluating a subset ofP1
, ...,Pn
, in that order. As long as the the result is null, continue. If this step uses all the operandsP1
..Pn
then the evaluation ofP
yields null. Otherwise we evaluated somePj
to a non-null objectr
, in which caseP
evaluates tor
.Consider a parenthesizedPattern
P
of the form(P1)
. Evaluation ofP
consists in evaluatingP1
to an objectr
, andP
then yieldsr
.A castPattern
P
of the formP1 as T
evaluates by evaluatingP1
to an objectr
. Ifr
has a run-time type which isT
or a subtype thereof thenP
evaluates tor
, otherwiseP
evaluates to null.A nullCheckPattern
P
of the formP1?
evaluates by evaluatingP1
to an objectr
. Ifr
is not null thenP
evaluates tor
, otherwiseP
evaluates to null (that is,P
evaluates tor
in all cases).A nullAssertPattern
P
of the formP1!
evaluates by evaluatingP1
to an objectr
. Ifr
is not null thenP
evaluates tor
, otherwise the evaluation ofP
completes by throwing an exception.The remaining patterns always have result type
bool
(constantPattern, variablePattern, and identifierPattern), and they evaluate to true if the match succeeds, otherwise they evaluate to false.Of course, an implementation may be able to lower patterns containing return patterns and get a behavior which isn't observably different from the semantics specified above without using any function objects, which is perfectly fine (even preferable because it is likely to be faster). However, it is important that the evaluation of a selector chain is only done in the case where the given return pattern "contributes to the successful match". If, in the end, the match fails, then we shouldn't have executed any of those selector chains. Similarly, if we have
P1 || P2
andP1
fails butP2
succeeds then we must execute the selector chain forP2
, at the very end, but it is not allowed to execute the selector chain forP1
.Versions
=> expression
part, e.g.,41 case int() && final value => value + 1
.The text was updated successfully, but these errors were encountered: