-
Notifications
You must be signed in to change notification settings - Fork 207
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
Type literals in switch cases are confusing. #2911
Comments
I am already familiar with the syntax
I am almost certain that this use case will rarely be used in "client" code compared to type checking, and because of this, I am in favor of Dart changing the meaning of Here are some syntax alternatives: num number = 3.0;
switch (number) {
case is Int:
case is Double:
}
// if issue dart-lang/linter#2090 I like this for checking the Type object
Type<num> type = int;
switch (type) {
case is Type<int>:
case is Type<double>:
}
// or without explicit syntax (is) for type checking
num number = 3.0;
switch (number) {
case int:
case double:
}
Type<num> type = int;
switch (type) {
case Type<int>:
case Type<double>:
} |
Disallowing I don't think there is any need to add new syntax, we already have two ways to do either operation: Adding a third syntax for type check isn't adding much. I'd love for But maybe the problem will go away if we get a good warning for unrelated-type patterns, |
I have been worried about this exact footgun for basically the entire time I've been working on the proposal. I know you and I have talked about it and I've had long conversations about it with Leaf. I was never able to come up with a great solution. :( Other languages sometimes address this by having different naming rules for types and values, so they can say a capitalized identifier is always a type test. But we are cursed with
Instead of switch (someType) {
case == int: print('int type');
case == String: print('String type');
} Either way, I like this idea.
I believe I've heard anecdotally that switches on type objects are a fairly common pattern in some kind of code? Maybe something to do with tests or factories? I can't recall.
We've discussed this. The problem is that it doesn't compose. We could make How about we add a lint for using a type literal as a switch case with a quick fix that puts One thing that's somewhat helpful: If you're trying to type test on a sealed type and you accidentally do a type literal instead of a type test, you'll get an error that the switch isn't exhaustive. |
If we disallow type literals as patterns, then the record pattern If we give precedence to parsing a type as a pattern over parsing an expressions, then Treating a type literal as If we do nothing, then |
I worry about this footgun a bit too. Code-completion could be possibly get tuned to nudge people away from the mistake. Currently we make this way too easy: (fyi @bwilkerson) But even that is only small measure. Diagnostics seem like a good way to go.
On the surface this sounds like a good idea to me. Is this correct? BAD switch(o) {
case int:
case String:
case (String, int):
} GOOD switch(o) {
case == int:
case == String:
case == (String, int):
} |
I think we've settled on leaving the feature as specified and trying to route around the footgun with lints: dart-lang/sdk#59087 |
We haven't actually created the lint, so I think it's too soon to close this out. It's a significant foot-gun in our syntax. I actually think it should be an on-by-default warning instead of a lint. |
Shouldn't a lint for checking unrelated types work? Like: switch ("str") {
case 42: // dead code
} The root of the issue is that we're effectively checking that a Type is == to a non-Type object. |
Every type is related to Map<String, dynamic> json = ...;
if (json case {"kind": "value", "value": int}) print("is integer value"); Pattern matching JSON structures will probably happen a lot. Most values are typed as Same for handling other untyped messages, like isolate port events: port.handler = (message) {
switch (message) {
case ("number", int):
case ("number", double):
// Might actually fail here if `message` is promoted to `(String, Type)`.
handleNumbers(message.$1);
// ...
}
}; Pattern-based switching is a way to figure out the type of an untyped object. Using "unrelated type" to catch bad patterns just doesn't work in that case. |
They'd get a compilation error since Type is not a number, so I'm not sure that's an issue. I don't think Object/dynamic being converted into Type is an issue. I'd expect most use cases to have an interface other than Object/dynamic as input (typically a sealed class). In which case an unrelated type check would work. |
Have you tried it? In DartPad, master branch, running: void main() {
var message = ("number", 42) as dynamic;
switch (message) {
case ("number", int):
case ("number", double):
// static type of `message` is `dynamic`.
handleNumbers(message.$1);
// ...
}
}
void handleNumbers(num value) {
print(value);
} I get zero warnings, zero errors. It just doesn't match. If we could promote to So the static type of Arguably, the biggest problem is starting with JSON is almost always typed as My bet is we'll see a lot of questions of "why doesn't this match" from people trying to match JSON, and using type literal constant patterns where they intended a typed variable or object pattern. I could be wrong, and nobody runs into this issue. I hope I am. |
I've his this twice while writing sample code just the past week. Both times I was super confused about what was happening... |
@lrhn I do get an error. But upon further look, it seems to be caused by I'd probably end up refactoring the code to: var message = ('number', 42) as dynamic;
switch (message) {
case ('number', num second):
handleNumbers(second);
// ...
}
handleNumbers(num nbr) {...} Although this isn't quite the same behavior as what you shared. I originally naively thought we could do: switch (message) {
case ('number', int second):
case ('number', double second):
// "second" is downcasted to "num"
} |
Adding to that, I'm surprised I guess that's because the pattern match means the property is known. But then why is I'd expect that if |
There's no dynamic call when destructuring the fields in the pattern because the record pattern has already type tested that the value is a record first. It's almost as if every pattern binds a new hidden variable whose type is the pattern's tested type, and then further destructuring operates on that hidden variable.
We don't actually promote variables of type |
We actually do promote from void main() {
dynamic d = 1;
if (d is int) {
var l = [d];
print(l.runtimeType); // JSArray<int>, so `d` has static type `int` in the line above.
}
} It's the pattern which promotes to dynamic d = (1, "A");
if (d case (1, "A")) {
// Assumes pattern matched, so `d` is promoted to `(X, Y)`
// where `X` is the type promoted by `d.$1 case 1` and `Y` by `d.$2 case "A"`.
// But `d.$1` has type dynamic and `case 1`, or any const pattern, doesn't promote
// so result is `(dynamic, dynamic)`.
var l = [d];
print(l.runtimeType); // JSArray<(dynamic, dynamic)>
} Same for So the |
I guess what confuses me is: why does After checking more, I don't understand why the following doesn't work: Object? a = (42, "a");
if (a case (42, "a")) {
String second = a.$2;
} That's probably the root of my confusion when discussing the impact of the I'd naively think that doing Is the issue that objects could somehow override ==, such that a non-string could be "equal" to "a"? (like how 42.0 and 42 are equal)? Of course, this wouldn't work for classes as they could have a getter instead of a field. But for records, I'd expect this to work. Although I'm likely missing something. |
Because the pattern matching doesn't guarantee anything about the type of the matched value, in general, because pattern matching uses All we know about While we can actually know something in this case, because As you mention yourself, even So an class Wat {
const Wat();
bool operator(Object other) => true; // Everyone's equal!
}
// ...
if (a case (const Wat(), const Wat())) { ... } then the pattern check succeeds, but I know absolutely nothing about the type of TL;DR: It could possibly work, for constant patterns only, when the constant has primitive equality, and we'd have to specify the behavior for every such type in the language itself. We haven't attempted to do that. |
That's fair. I guess my intuition comes from typescript, which does handle cases such as Still, with Maybe the issue could be solved with a bit of education on how to deal with |
Will close this issue when the lint is released in the |
What I don't understand why this switch (T) {
case const (String):
await prefs.setString(key, value as String);
break;
case const (int):
await prefs.setInt(key, value as int);
break; when I suddenly got the linter error after upgrading Flutter I was quire annoyed because I didn't see any benefit of adding the additional |
I think this is done now, so I'm going to close this.
It's not necessary. It's just that the code is—to some readers at least—more confusing without it. If you just do If we had a time machine, we would have designed Dart around patterns initially and avoided ever having type literals or allowing them as constant value patterns. But we don't have a time machine. In the absence of that, the best we can do is try to route users away from existing but confusing syntax. |
When you start switching over sealed type hierarchies, it's incredibly easy to write:
when you mean to do a type check for
TheType
. Instead it compares to theType
object for the type literalTheType
, and fails. With luck, you're told that the switch isn't exhaustive, but you still have to figure out why. (I'm guessing that we'll get warnings about unrelated types eventually, which will make it easier to spot the problem.)I did this, as recently as yesterday.
Others do it too, like a stack-overflow question.
Very little switch-pattern code has been written, and it already crops up, so I fear it might be serious useabilty stumbler.
Maybe we should protect people against this by disallowing type literals as constant patterns without a leading
const
.At least that will lead people to do either
case TheType _:
/case TheType():
orcase const TheType:
, depending on which one they want.The big issue is that then existing switches over
Type
objects will need migration. I don't know how many such switches are, but probably non-zero.(We could also change the meaning of
case TheType:
to becase TheType _:
, but that risks being confusing in the other direction. If we just prevents the syntax now, it be possible to give it back later in either case.)The text was updated successfully, but these errors were encountered: