Open eernstg opened 1 week ago
1) i'm for case
as expression, since imo case
should be expression since Dart 3, and shouldn't be left as it's own language construct.
2) i'm really iffy about the return
pattern, or at least the specification as it stands. This is based on the example you've provided, where it took me considerable effort to understand what is actually being returned (without looking at the desugared code).
also, in the grammar for the case
expression, is there any need for the with / without cascade
division? From what i see, the grammar for both is the same, so unless i'm forgetting something, it could be a single rule, right? Could you please clarify why is not?
I find some of the return
patterns pretty hard to grok. In general, I think the examples here don't do a great job of motivating the feature, maybe there are some better ones that would be more convincing? For the examples above, I think there are better ways to write them in Dart as it exists now.
// 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 case
being an expression / operator, then i'm actually for it. I'm sure that i'm not the only one that has wanted to use case
in loops or tried to use case
as a comparison operator. I have already thought about wanting to use case
as a normal operator rather than as it's own construct.
if we are talking about the return
pattern, i am against it purely because i can't read it. If there was more readable way to do it, i wouldn't mind.
I'm sure that i'm not the only one that has wanted to use
case
in loops or tried to usecase
as a comparison operator.
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 case
as expression / operator, it took me a long time to find an actual use-case, which made me lose confidence in this feature.
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: 1) boolean / filtering functions
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;
});
2) combining while(true)
and if() break
+ possible destructuring inside the condition
// 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 case
expression, and in the while
case, they would be passed to the block.
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 while
example is compelling, and indeed there is already an issue tracking this from when we shipped the original feature.
The boolean
case has also been raised elsewhere.
I'd suggest giving those issues a thumbs up since I think they will likely remain the canonical issues for those specific sub-features.
@eernstg:
Are going to support this, too?
var x = a > b && return a || return b;
Are going to support this, too?
@tatumizer i suppose not. Disregarding the missing case
keyword, patterns currently don't allow comparing with non constant expression, meaning it would be a compile error.
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 a?.b?.c
: The expression as a whole evaluates to null if "anything went wrong" during the evaluation, and the enclosing code can detect this and act accordingly.
It could be argued (for expressions using null shorting like a?.b?.c
as well as for case expressions) that we can also get null in other ways (e.g., a
is non-null, a.b
is non-null, but a.b.c
is null). We must take care to avoid interpreting the result null to mean "the situation I required doesn't exist" if it really means "the situation does exist, but we successfully looked up null". However, this doesn't seem to create huge issues today (e.g., with null shorting), and it should also be OK to have to handle it with case expressions. After all, the exact same issue comes up with a switch expression that includes a default case of the form _ => null
.
// 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 _currentElement
isn't a StatefulElement
, or if it is a StatefulElement
whose state
doesn't have the type T
(which is a type variable of an enclosing class). In other words, we're describing a situation that includes several assumed types, and we're using (_currentElement as StatefulElement).state
if everything is OK. If anything isn't OK then we just return null. We can say this concisely because "anything isn't OK" is an implicit property of several parts of this expression.
// 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<Object?> 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 e case T return
works like the safe cast operation which is requested in #399, that is e as? T
. Another reason why the expression is concise is that we can use return
in two different OR-branches of the second case expression, using the value of the key 'name' in one case and 'path' in the other case.
// 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 findChildIndexCallback
is null.
// 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 AssertionError
whose message
is a FlutterError
then we will use that message
, if we're looking at a FlutterError
we'll use that, and everything else will be ignored (that is, replaced by null).
// 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 decoration
when focused, "returning" its focusedErrorBorder
if it has an errorText
, otherwise its focusedBorder
; and similarly for the two cases where it isn't focused.
@hydro63 wrote:
is there any need for the
with / without cascade
division?
It allows a case expression to occur in a location where an <expressionWithoutCascade>
is required. The main case would be in cascades:
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 ()
around the case expression, but that may seem inconvenient, especially if viewed by someone who knows that this requirement could easily have been avoided. ;-)
I'm sure that i'm not the only one that has wanted to use case in loops
The special exception about case expressions in if
statements is that the variables they declare are in scope in the body.
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 when
clause of the case expression itself, if any.
@leafpetersen wrote:
I think the examples here don't do a great job of motivating the feature
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:
Are going to support this, too?
var x = a > b && return a || return b;
I don't quite know how this would generalize, but var x = a > b ? a : b;
seems to do the job for this example.
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 use of return
in expressions has been a subject of debate.
Some commenters suggested that the syntax of conditional expression cond ? a : b
can be expanded by adding support for return
(after all, we allow throw
, which makes return
a legitimate option).
However, the suggested meaning of return
in var x = a > b ? return a : b
was different: it was supposed to signify the return from the containing function.
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? If not, on what grounds?
The use of
return
in expressions has been a subject of debate.
Right, that's probably https://github.com/dart-lang/language/issues/2025. That would be a separate proposal, and it wouldn't conflict with this one (in this proposal return
is parsed as a pattern, but in #2025 it is parsed as an expression).
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.
}
.. the suggested meaning of
return
invar x = a > b ? return a : b
was different: it was supposed to signify the return from the containing function.
Exactly. And the parser will have no difficulties knowing that return a
is an expression in this case.
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 o
; if it's an A
then the case expression evaluates to (o as A).a
; if it's a B
then the case expression evaluates to (o as B).b
; otherwise it evaluates to null.
In line 1 of the body of foo
, this result is stored in x
. In line 3 it is discarded. That's OK, even though it wouldn't be very useful in this case. If you just want to have the side effects of calling a
respectively b
then you could do o case A(a: _) || B(b: _);
.
That would be a separate proposal, and it wouldn't conflict with this one (in this proposal return is parsed as a pattern, but in https://github.com/dart-lang/language/issues/2025 it is parsed as an expression).
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 case
keyword that would suggest a different interpretation of the subsequent "return". Or you think there is?
Unrelated: I think you are overdoing it in B(b: return.length)
. Is there a precedent for this? Could you also write B(b: return +1)
? Or the syntax is reserved for chains only? Why not require the explicit value in return
, like:
B(b: return b.length)
I've wanted case-expressions for a long time. Wanted them in a while
statement today, and have wanted them in conditional expressions and for-loop conditions before.
Had to day what the scope of the variables bound by the test expression is. Definitely anything guarded by the test .
Probably only that, like the variables of an if
-case
only begging in scope in the then-branch.
The return
pattern... Not sold!
It feels like it's mixing expressions and patterns, and I think that's a very bad idea for readability.
Something like e case (P1 && return) || (P2 && return)
looks like JavaScript logical expressions.
Just do a switch, or add expression variable declarations and do
((var tmp = e) case P1) ? v1 :
(tmp case P2) ? v2 : null
It's not totally clear that return
short-circuits pattern evaluation. It probably has to, but that means it can only occur in tail position.
You can't do: case: return && P2
because P2 is unreachable.
(I can see why using a control flow keyword makes sense. Why not break
? If we get break-with-value for breaking out of expressions, this mighty fit that pattern better than return. After all it's function local control flow.)
Otherwise, the best way I have found to think about the return
is as a special variable binding for the implicit " Return value variable". Which also short-circuits.
The retuen.focusedErrorBorder
breaks that understanding. It looks like something that happens after returning. And it's not a pattern. The pattern would be _(focusedErrorBorder: return)
.
The pattern e case SomeType return
occurs a few times. That's precisely what e as? SomeType
would do, and I'd prefer as?
for that.
(I'm fine with making return
an expression, but then it will actually return from the curb m current function.)
FWIW, among the examples given above in https://github.com/dart-lang/language/issues/4141#issuecomment-2437380099 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 👍 :smile:
My biggest problem with return
pattern is that i don't really know what is being returned, even after reading it multiple times. If they at least had clearly marked return value (=> val
), just like switch expressions, i would be able to sort of get it. But even then, i'd probably get lost in the multiple branches that you would try to stuff into a single pattern.
The return
pattern is a hard no for me.
@tatumizer wrote:
There's nothing magical in the
case
keyword that would suggest a different interpretation of the subsequent "return". Or you think there is?
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 on
and hide
that are known to the grammar). It is certainly possible that this can give rise to confusion, especially for readers for whom the new construct is unknown, or at least not very well-known.
However, we have lots of situations where these special words have different meanings in different contexts, and so do other terms. For example: The on
clause of a mixin is completely different from the on
type of an extension, and from the on
clause of a try/catch statement, and from a variable or type whose name is on
. The extends
clause of a class/enum declaration is completely unrelated to the use of extends
to declare an upper bound for a type variable. The as
clause of an import directive is completely unrelated to the type cast expression that uses as
as an operator. And so on.
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 x
and y
below). In a refutable context it denotes a reference to a constant variable in scope (like c
in case c:
).
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 return
is encountered in a pattern, it is a (or is part of a) return pattern. Recognizing this will be a natural step to take for anyone who reads the code (as soon as they've gotten acquainted with return patterns, of course).
Unrelated: I think you are overdoing it in
B(b: return.length)
. Is there a precedent for this? Could you also writeB(b: return +1)
? Or the syntax is reserved for chains only? Why not require the explicit value in return, like:B(b: return b.length)
The idea is that a case expression e case P
or e case P when b
allows us to evaluate an expression (e
) and explore the result (by matching the pattern P
against the value of e
), confirming that it satisfies some requirements (specified as subpatterns of P
or in b
) and navigating from the given object as needed (which is also part of pattern matching), and then specify the value of the case expression as a whole in terms of the matched value at the point where a return
pattern is being matched.
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 return e
where e
is an <expression>
) are at least the following:
return e
then we would have to introduce a name for the matched value. It could be a new, predefined name (a "context-dependent reserved word") like matchedValue
or it
; or it could be a special word like this
; or we could require that the name is somehow already declared by the developer (as in B(b: return b.length)
, but it isn't obvious how we'd handle e case P && return
because return
needs to return the matched value which is the value of e
, but we don't have a declared name for it at that point). So this is a problem that we'd have to solve.Note that the ability to have a selector chain does not introduce extra syntax in the basic case where we just use a plain return
.
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:
I've wanted case-expressions for a long time. Wanted them in a while statement today, and have wanted them in conditional expressions and for-loop conditions before. Had to day what the scope of the variables bound by the test expression is. Definitely anything guarded by the test . Probably only that, like the variables of an if-case only begging in scope in the then-branch.
Sounds like case expressions whose pattern does not contain any return patterns. Very good, that should just work out of the box for while
and ?:
.
I'm not so sure about for
... where would the case expression occur?
We agree completely about the scoping, that variables introduced by the pattern are in scope in the body of the while
loop and the first branch of the conditional expression (that is, for b case P when b ? e1 : e2
, variables declared by P
are in scope for e1
).
The return pattern... Not sold!
Surprise, surprise. ;-)
It feels like it's mixing expressions and patterns, and I think that's a very bad idea for readability.
return
statements have two aspects: (1) They specify the value which will be the result of the current invocation of the nearest enclosing function (for return e;
it is the result of evaluating e
and for return;
it is null). (2) they cause the execution of the current function body to complete returning said value.
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: e case C(c: return) || D(enabled: true, d: return)
is fine, just like switch (e) { C(c:final T result) || D(enabled: true, d:final T result) => result }
, but e case C(c: return) || D(enabled: true)
is an error because the second subpattern of the OrPattern leaves the returned value undefined, and e case C(c1: return, c2: return)
is an error because it leaves the returned value overconstrained.)
For e case P
, if the value of e
fails to match P
then the value of e case P
is null. Otherwise, the value of e case P
is the result of evaluating the given selector chain on the given matched value.
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 i
, obtain 1
, mark it as the matched value (on the condition that the first subpattern of the OrPattern matches), then invoke b
, obtain false, and determine that the first subpattern of the OrPattern did not match, then unbind the matched value, and proceed with the second subpattern of the OrPattern: obtain the value of b
from cache, confirm that it is false, invoke j
and obtain 2
, then conclude that the second subpattern of the OrPattern matched, so the entire pattern matched, and the matched value is 2
, and the associated selector chain is .effect()
, which means that Working on 2
is printed, and x
is initialized to have the value 12.
Something like
e case (P1 && return) || (P2 && return)
looks like JavaScript logical expressions.
Surely they have a different semantics. I'd just recommend not thinking in terms of JavaScript when trying to read Dart code.
Just do a switch, or add expression variable declarations and do
((var tmp = e) case P1) ? v1 : (tmp case P2) ? v2 : null
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 e case (P1 && return) || (P2 && return)
, it would be guaranteed that P1
and P2
do not contain any return patterns (because that would be a compile-time error because it over-constrains the value), so you should be able to use any of the following:
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;
}
It's not totally clear that return short-circuits pattern evaluation. It probably has to, but that means it can only occur in tail position.
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.
You can't do:
case: return && P2
becauseP2
is unreachable.
Yes, you can, but it's a compile-time error if P2
contains any return patterns. The value of the case expression will be the value of the expression at the left end (omitted above), or null if P2
doesn't match.
(I can see why using a control flow keyword makes sense. Why not break? If we get break-with-value for breaking out of expressions, this mighty fit that pattern better than return. After all it's function local control flow.)
break
would signal "this is about control flow" and that's the opposite of what I want.
Otherwise, the best way I have found to think about the
return
is as a special variable binding for the implicit " Return value variable". Which also short-circuits.
That sounds right! (except that it doesn't short-circuit anything)
The
retuen.focusedErrorBorder
breaks that understanding. It looks like something that happens after returning. And it's not a pattern. The pattern would be _(focusedErrorBorder: return).
It is a pattern because the proposal specifies that it is a pattern. ;-) Just like 1_000_000
is an integer literal because we specified that it is an integer literal, even though it wasn't previously.
The return pattern return.focusedErrorBorder
differs in a subtle way from _(focusedErrorBorder: return)
in that I'm proposing that the former evaluates theMatchedValue.focusedErrorBorder
if this return pattern is part of the successful matching. The latter, _(focusedErrorBorder: return)
will evaluate focusedErrorBorder
during matching.
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 P
in a case expression of the form e case P
. You could say that "only the return pattern whose match was used to obtain a successful match will have its selector chain executed."
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 (e case P).the().selector().chain()
) is that the matched value may have different types in different subpatterns, and we may also wish to do different things with it before it can serve as the result of the case expression.
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.
The pattern
e case SomeType return
occurs a few times. That's precisely whate as? SomeType
would do, and I'd preferas?
for that.
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'm fine with making return an expression, but then it will actually return from the curb m current function.)
I'd like that, too!
But an expression of the form return e
or return
(returning null) is a different topic, so I won't go into that here.
@hydro63 wrote:
My biggest problem with
return
pattern is that i don't really know what is being returned, even after reading it multiple times.
I hope this helps: For a plain return
, it's the matched value where the return pattern occurs:
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 T return
, it's the matched value where this return pattern occurs, but it only succeeds if the matched value has type T
. For a final return id
it provides two ways to access the matched value: (1) it determines the result of the enclosing case expression, if the pattern where this return pattern occurs succeeds (that is, same thing as a plain return
), and (2) it introduces the identifier id
(which could be used just like other variables introduced by this pattern, e.g., in a when
clause). Similar rules work for final T return id
and var return id
.
For a return pattern with a selector chain, e.g., return.m(expr)
, if the matching step succeeded, and this return pattern was part of the successful match, and the matched value was o
, then the value of the case expression is o.m(expr)
.
@eernstg:
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. :smile:
(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 return
to also have a when
clause. At least a pattern with a return clause can exit without binding later variables, so the when
clause can't refer to any variable that occurs after a return
.
But I guess each return is at the end of a ||
separated pattern, and those don't share variables unless they all declare the same variable.
The return
just reads too weirdly. It's a refutable pattern, and defaulting to null
for "no match" (and presumably a failing when
?) is more default than I'd like.
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.
@tatumizer
you have to scan the sentence back and forth and translate it to a simpler representation in your head
var x = A(false, 1, 2) case A(i: return.effect(), b: true) when i > 15;
Let's try.
We can skip A(false, 1, 2)
because the scrutinee expression is just an expression, and it shouldn't be harder to read than any other expression in the language. So let's assume that we can read the expression and see case
rather easily, or at least with no greater difficulty than other expressions in the language.
Next, the keyword case
itself indicates that the following thing is a pattern. This is true for switch statements and for if-case statements, so that should be a well-known signal.
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(b: true, i: return.effect())
rather than the opposite order, but we still need to know how the program should behave in both cases.
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 return.effect()
is executed by (1) replacing return
by the matched value v
, and (2) computing the expression v.effect()
, which is then the value of the case expression as a whole.
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 return.effect()
to be executed in the case where the enclosing pattern fails to match. One perspective that you could use is illustrated by the following rewrite:
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 pattern with a return clause can exit without binding later variables, so the when clause can't refer to any variable that occurs after a return.
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 when
clause can refer to variables in the same way as today.
But I guess each
return
is at the end of a||
separated pattern
I'm sure that would be a very natural way to use this feature.
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];
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 <patternVariableDeclaration>
.
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 let
expression) is in an irrefutable position. This means that we can't do anything that isn't guaranteed to succeed.
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,
};
I'm sure that would be a very natural way to use this feature.
But not necessary. Got it!
The return
does not actually return, it just binds an anonymous variable that then becomes the result of the pattern match expression when it completes successfully. Like a Pascal return value assignment.
That's an easier model to reason about.
Then return.foo
is a questionable feature. If the selectors are evaluated in the middle of the pattern, then it's inconsistent. (If some of the same selectors have already been cached, should the cached value be used?)
If they're only evaluated at the end of a successful pattern match, then evaluation order is harder to grok.
If anything, I'd just treat return
(or heck, use yield
), as a special variable name.
Binding that name makes the pattern match expression evaluate to a user chosen value if it matches, and null
if not, instead of true
/false
.
There is no new pattern syntax, it's just that "variable name" contains an extra reserved word.
(And the type of the expression can be non-nullable if the pattern is irrefutable and binds the value on all paths).
But then, having to write var return
isn't an improvement on return
.
Still not sure the feature is worth it. It's just a shorthand for switch (e) { ... var it ... => it}
plus , _ => null
if refutable.
It's also not general enough to, fx, extract two values.
If I have two lists and want the last element of each, I can't do (pair case ([..., return], [..., return]))
, so I'll still have to do switch (pair) { ([... var first], [... var second]) => (first, second)}
.
Or use an extension plus selectors, if we allow those, to do (pair case ([..., var first], [..., return.pairWith(first: first))]))
.
(I worry about features that make people want to do workarounds. Either they're not powerful enough, because they can't do what people really want, or they're slightly more powerful than they should be, because it's not something people should do to begin with.)
I think I'd rather just tell people to do:
(pair case ([..., var first], [..., var second])) ? (first, second) : null
Explicit over implicit, for the null
too.
If we wanted to add add anything for that, it's an if
expression with optional else
(defaulting to else null
) so you can do:
if (pair case ([..., var first], [..., var second])) (first, second)`
(Or not, that doesn't really read well.)
Like a Pascal return value assignment. That's an easier model to reason about.
Exactly!
return.foo
is a questionable feature. .. If they're only evaluated at the end of a successful pattern match, then evaluation order is harder to grok.
That's true. My assumption is that it is really tempting to allow return.foo()
when we'd otherwise have to write a somewhat longer switch expression to get the same thing:
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 case?
expression with a single case, returning null on no match. The syntax is the exact copy of the case
statement used in the switch
expression, just with the added suffix ?
.
// 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 switch
for n=1)
A switch?
that never needs to be exhaustive, if its an expression it evaluates to null
if there is no match, would be a good match for a switch!
that is always forced to be exhaustive.
The latter is something you may sometimes want for switch statements, when inference cannot recognize that the switch is actually exhaustive.
A switch?
expression that simply adds on a _ => null
case at the end would be nice. And a switch!
statement that always requires exhaustiveness would be nice. No, make that "Really Nice!"!
I do agree that we could make this proposal a bit less radical by dropping the return patterns and having =>
in the case expression. But I'll continue to push on the return patterns because I think they provide a significant amount of extra immediacy and conciseness.
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 case?
proposal wouldn't suffice anyway) and do something like this:
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 result
have the same type. In that case it can't be the same variable, unless that variable has a type which is a common supertype of those two types, and then it's unlikely that we can do what we want to do (unless we use the type dynamic
). Moreover, a single expression after the =>
would not easily be able to treat the result
differently.
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 think the return patterns have the potential to be more readable, when we've gotten used to return patterns.
I do like the =>
idea. It's like an inline single-case switch.
You can do:
switch (m) { p when c => e }
or you can do
(m case p when c => e)
Grammar migth be a problem. The c => e
can look like a function expression.
(m case p when (x) => y)
A function literal will never be a bool
expression, so maybe we can get away with the condition being an "expression-no-function-literal", like the expressions of a constructor initializer list.
A case
-expression with no =>
evaluates to a boolean and binds variables on a true-branch if immediately branching on the result.
A case
-expression with a =>
only binds variables in the result expression.
But maybe the =>
-form just isn't worth it.
It's not much shorther than the single-case switch. A switch?
would let you omit a _ => null
case, and otherwise it's also not big, or you can do:
(m case p when c) ? e : null
for the same effect, which is only 4-5 characters longer than (m case p when c => e)
, the cost of having to write the null
explicitly. (Or allow (if (m case p when c) e)
with no else
to work as an if
-expression with an implicit null
else
-branch, which is only 2 characters longer than the =>
format, but isn't specific to pattern matching.
The big enabler here is to make (m case p when c)
and expression, then any branch logic can be used with it.
Adding syntax that only works with pattern matches is less powerful than that. Adding both might allow some cases to be slightly shorter, but I'm not sure it's enough to be worth it. And the embedded return
just doesn't read well for me.)
(m case p when c) ? e : null
The problem is that the variables defined in pattern p must be available in ? e
part, but not in : other
part. This is a bit problematic. The whole point of case?
variant (with =>) is to eliminate this problem.
But if we support case?
, it might make sense to support also something like when? cond => p
for a general condition cond
:
var a = when? b > c => d
.
I'm pretty sure we would want to have a special rule about conditional expressions (?:
) saying that variables introduced by a case expression in the condition are in scope in the first branch. Just like the special rule that we have about if
statements saying that such variables are in scope in the "then" branch.
That's also what I've chosen for the proposal in the OP of this issue.
... saying that variables introduced by a case expression in the condition are in scope in the first branch.
For this, you have to disallow shadowing (which is what java does), otherwise
var x = 0;
var y = foo case Foo(:final x) ? x+1: x-1;
will be a valid expression, where the meaning of x
is different in different branches.
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. https://github.com/dart-lang/linter/pull/872 didn't fly, but similar lints could be considered.
It didn't fly because it was too broad and too restrictive.
Here, we are talking specifically about disallowing shadowing in patterns only.
This was a show-stopper for negative case
conditions.
Java (generally) allows shadowing, but not in this scenario (I posted some examples of related java code, but I can't find where b/c github search is no good, so I don't remember exactly what "this scenario" means, but there's certainly some scenario :smile:)
But sure, if dart today allows shadowing in patterns, and has no plans of disallowing it, then what you are suggesting is not worse.
However, now you have to agree that ?:
operator is always sufficient:
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? :smile:
@lrhn: an extra argument in favor of switch?
is that it naturally morphs into a "switch without scrutinee".
Without it, the switch of the form
var a = switch {
cond1 => v1,
cond2 => v2
};
would often require an extra line _ => null
- which would become redundant if you use switch?
instead,
If switch?
is introduced, it would add more weight to the idea of case?
: the rhyme between switch?
and case?
legitimizes both.
The advantage of switch?
over ?:
with several subordinate branches is that the former is more readable and lends itself to better formatting. Then the style guide may suggest using ?:
only in the simplest (n=1) case.
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 declaration return pattern of the form
T return id
,T return
, orfinal T return id
isT
. The result type of a declaration return pattern of the formfinal return id
orvar return id
is the matched value type of the pattern.A declaration return pattern of the form
final return
,final T return
, orvar return
is a compile-time error.The result type of an expression return pattern of the form
return
is the matched value type of the pattern. The result type of an expression return pattern of the formreturn s1 s2 .. sk
wheresj
is derived from<selector>
is the type of an expression of the formx s1 s2 .. sk
wherex
is a fresh local variable whose static type is the matched value type of the pattern.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 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
.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 declaration return pattern of the form
T return id
,T return
, orfinal T return id
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 declaration return pattern of the formfinal return id
orvar return id
evaluates to() => v
wherev
is a fresh variable whose value is the matched value (and it never fails to match).An expression return pattern of the form
return
evaluates to() => v
wherev
is the matched value. An expression return pattern of the formreturn s1 s2 .. sk
wheresj
is derived from<selector>
evaluates to the value() => v s1 s2 .. sk
wherev
is again a fresh variable bound to the matched value.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