-
Notifications
You must be signed in to change notification settings - Fork 213
[Patterns] Should List and/or Map patterns be considered for exhaustiveness? #2693
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
Comments
This is actually what the proposal currently states, but it's not worded very clearly. It says:
Note that the first bullet point makes no mention of whether the matched value type is an exhaustive type or not. That means switch expressions must always be exhaustive regardless of the type you're matching on. I'll add some clarification.
Yes, we will define exhaustiveness for all types. The "exhaustive types" part of the proposal has only one purpose: if defines (by negation) the set of types you can switch on in a switch statement that don't lead to a compile time error if the switch isn't exhaustive. "Exhaustive type" is probably a confusing choice of term, but it was the first one that came to mind. I'll see if I can come up with something better. Maybe "statement-exhaustive type" or something? |
In switch expressions, we do want full exhaustiveness and this switch isn't exhaustive.
I think it would be good to require that all subtypes be covered too, which, if you have a list pattern or match on
I would be happy to have exhaustiveness be able to reason about integer ranges and use that for length checks in list patterns too. It would be very cool if exhaustivess was smart about relational patterns on integer types. But if we can't make that work, it's not the end of the world either. Right now, exhaustiveness is just not fleshed out at all for lists. I think there's a big TODO in the proposal for it. |
I think exhaustiveness for lists (and maps) would have to use an irrefutable case ( Those lists (maps) would of course be "bad", but exhaustiveness is used in flow analysis and hence it must be sound. I'd think this means that exhaustiveness for lists and maps is trivial, but it always relies on a catch-all case. |
I'm not sure what you're saying here. Which of the following non-exhaustive statements do you propose should be errors/not errors? switch([3]) {
case MySpecialList(): print("Special List");
}
switch(<Object>[3]) {
case List<int>(): print("List of int");
}
switch([3]) {
case []: print("Empty");
} |
Ah right. Paul mentioned that recently too. Filed an issue: #2701
I'd like to avoid requiring a pointless default case for lists, but I agree that whatever we do of course must be sound.
I think none of those should be compile-time errors because they are switch statements. If they were switch expressions, they all would be. In other words, I don't think lists should be an always-exhaustive type. |
This matches my understanding, but I was confused by:
Maybe you were moving back to switch expressions here? Which again comes back to the main clarification which I take out of this issue, which is that:
|
Re-opening because I missed your last example which is a really interesting one:
I suspect that users will be surprised that What I really don't want is users to end up preferring to jam switch expressions into statement contexts just to get tighter exhaustiveness checking like your second example: void test(A a) {
int x;
(switch(a) {
A(value:true) => x = 0,
A(value:false) => x = 1
})
x.isEven; // Ok, x is definitely assigned.
} I don't have a good sense of:
@pq, do you think it would be possible to get any data on #1? It requires static analysis to know what static type is being switched on and whether the cases in it are exhaustive or not. I suspect that we can live with the current proposal but this is definitely worrisome. |
I'd be fine with exhausting lists if we can show that any list of the input type is definitely covered by at least one case. I don't want list types to require exhaustiveness in a non-expression It's the same thing as the Such exhaustiveness is not fragile in the same ways as enum-values or sealed-subtypes exhaustiveness, where adding a new value or subtype will change the behavior of switches. It's not based on things external to the type, which can be changed independently, but only on the type itself. If you match the type Object patterns can exhaust a type by exhausting all the properties being looked at, which is why: I'm not worried about giving users the advantage of exhaustiveness if they actually exhaustively match Only enums, Back to lists (and with the negative-length loophole considered closed): switch (boolList) {
[] => 0, // length 0 totally covered
[var b] => b ? 1 : 0, // Length 1 totally covered
[false, ..., false] => 0, // Length 2+ covered for [0]==false and [len-1]==false
[true, ...,] => 1, // Length 2+ covered for [0]==true
[..., true] => 1 // Length 2+ covered for [len-1]==true
} is probably exhaustive. switch (boolList) {
[] => ...
[var x] => ...
[var x, var y] => ...
[var x, ..., true, var y] => ...
[var x, false, ... var y] => ...
[var x, var y, ..., var z, var w] => ...
} gets trickier, because we need to cover length-3 lists using Maps are right out. Matching all lengths of maps is pointless because the structure of a map is not defined by its length, but by its keys. We'd have to exhaust the powerset of the key type to know we've exhausted all possible maps. Most maps being matched will be JSON, and the keyset is If we are considering exhaustiveness for lists when we match all lengths, then we should consider exhaustiveness for integers when we cover all ranges. Otherwise exhausting the list lengths feel a little under-defined. (How do we know we cover all integer lengths?) We can (and probably should) special case integer values, so that int sign(int integer) => switch (integer) {
case < 0 => -1,
case 0 => 0, // or: case == 0 => 0
case >0 => 1,
}; works. |
Ugh, yeah. I don't want to create any incentives to do this either. It feels to me like the crux of the problem is that since constant evaluation happens after flow analysis, and checking for exhaustiveness errors has to happen after constant evaluation, flow analysis has to make some assumption about switch statement exhaustiveness that might be wrong. If it assumes switches on lists are exhaustive (i.e. we make lists an exhaustive type), then users who don't want an exhaustive switch statement for their list are going to have to add a I wonder if we could have it both ways, though. We could:
This would mean that:
There is an implementation cost to this strategy, though, which is that the exhaustiveness checking algorithm would have to be written in such a way that it can be invoked either during flow analysis or after constant evaluation depending on the circumstance. My gut feeling is that would not be too much of a problem, but CC @johnniwinther in case he wants to weigh in. |
@stereotype441 My work so far indicates that this might be doable. |
Sure. One approach would be to take |
I would love love love to see data (especially from google3) on this if you have the time to dig it up. |
I think this was completed. |
The initial issue is a little vague and the subsequent discussion meanders (in part because of my mis-understanding of Leaf's issue and comments) but I believe the core issue is still open. There are two separate but confusingly similar concepts:
The list Leaf points out in the issue is the list of always-exhaustive types, which is what I think this issue is about. These are separate concepts because Dart has one foot on the imperative side and one foot on the functional side. That puts switch statements in a weird place. Consider: respond(String s) {
switch (s) {
case 'hi': print('hello');
case 'bye': print('later');
}
} There is nothing unsound about this function. If you call it with one of two expected strings, it prints another string in reply. Otherwise, it prints nothing. Is it correct that this function silently prints nothing on any other string? Is that what the author intended? If we wanted the language to be maximally paranoid, we would require all switch statements to be exhaustive over all possible incoming values. That would mean that every switch needs to either have a set of cases that exhaustively covers all values (using the exhaustiveness checker) or must have a default case. But, historically, Dart has never required that. It's happy to have switch statements that don't match any case (unless the type being switched on is an enum). It's a statement after all. You can have When we added exhaustiveness checking and patterns, it felt weird to me to still allow non-exhaustive switch statements for all types. If you go out of your way to define a So to split the difference, I specified a list of "always-exhaustive" types. These are types where a switch statement must be exhaustive when matching on that type. (Switch expressions must always be exhaustive for all types.) That list isn't arbitrary but it's... heuristic-based. I chose a set of types where I thought user intuition would be that it should be exhaustive. This issue is asking the question of whether a List of an always-exhaustive type or a Map of an always-exhaustive type should itself be considered an always-exhaustive type. In Dart 3.0, the answer is "no". But we could conceivably tighten this if we felt it was helpful for users. |
The patterns proposal lists a small set of types which are considered for exhaustiveness:
List
andMap
are not included in this list, which suggests that code like the following will not be considered for exhaustiveness checking:I believe the code above will be considered an error, since there is no default, and the type in question is not an exhaustive type.
Should we include
List
andMap
as exhaustive types? The code above seems like really good code to allow. On the other hand,List
andMap
are not sealed types, so we probably (possibly?) don't want to require full exhaustiveness:Should we treat
List
(and possiblyMap
) as types which are exhaustive types only WRT to length? That is, we require that all lengths are covered, but not necessarily all subtypes? And if so, to what extent do we generalize this to explicit calls to thelength
getter, as opposed to only considering explicit patterns. Does the following get an exhaustiveness error?This would seem to start to require more reasoning about integers than we necessarily want to do.
Perhaps the correct way to think about this is to separate out what we can show to be exhaustive from what we require to be exhaustive? That is, we say:
For flow analysis and type inference, we have proposed that reachability for switch statement be driven entirely based on whether or not the type of the scrutinee is an exhaustive type (or if there is an explicit default). This would imply that there are switch statements which will be assumed to be non-exhaustive, but which are in fact exhaustive, and could be proven so if turned into switch statements.
Example:
cc @munificent @eernstg @lrhn @kallentu @natebosch @jakemac53 @stereotype441 @johnniwinther
The text was updated successfully, but these errors were encountered: