-
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
Ambiguous map patterns a dynamic error? #2657
Comments
The general problem cannot be solved at compile-time.
Maybe for identical. We should assume that For equality, we probably shouldn't. To dynamically check whether any two of N keys are equal, we need to do N*(N-1)/2 checks (and that's assuming symmetry). We cannot tell which equality a map uses for lookup, so comparing the lookup keys might be irrelevant.
And we cannot tell from the outside which one it is. What we know, for absolutely sure, is that the The one hypothetical reason to look at the map pattern key's equalities anyway would be that we assume (without evidence) that the actual map keys and the map pattern keys will have the same type, and that the map uses the In this particular case, using (We'll have to compare integers and doubles in constant primitive equality comparisons then. It's probably not a problem. I hope it won't make anything behave differently between the web and native, not any more than they already do.) All in all, I don't want to complicate map patterns more than necessary, because the general issue is unsolvable, and the common case works well. I'd be fine with warning about identical constant keys in a map pattern, even without primitive equality. Anything more than that will not carry its own weight, the potential benefit will be too small. If you think of We can also warn if you have the same property name twice in |
const m = {3.14: "pi"};
// Error. The type of a key in a constant map can't override the '==' operator, but the class 'double' does.
// Try using a different value for the key, or removing the keyword 'const' from the map.
// • const_map_key_expression_type_implements_equals Could it be an option to produce the same error in case of map pattern? |
@lrhn wrote:
Exactly. I think this illustrates the point that it is misguided to pretend that we provide a guarantee about matching every key in a map pattern with no rest pattern, in the case where some key expressions have non-primitive equality. One possible approach could be to say that it is a compile-time error, something like:
This means that we will actually match every key in a map when the pattern seems to promise that we do just that (that is, all keys have primitive equality), and in the (rare?) cases where no such guarantee exists, the pattern must explicitly opt out of the length equality check by having It is still possible to have a confusing pattern match failure at run time because the map might have |
@sgrekhov wrote:
That would certainly be a safe bet, but I suppose there's significant support behind the idea that the language should be more permissive than today's switches. On the other hand, if we restrict map patterns like that now, we could generalize them later in any way we want. |
If we "desugar" a map pattern like I think that's OK. If we teach people to read map patterns as shorthands for checking length and values, then they'll understand the cases where that fails. Hopefully most users will never see the edge cases, because they just use normal Dart
I think that's going too far. There are lots of potentially useful classes with non-primitive (Who am I kidding? Map patterns will almost exclusively be used for JSON, which has string keys with primitive I think it's fine to tell users when they do something that looks like it's a mistake:
If you use keys without primitive equality, we can't tell you at compile time whether you are maybe repeating yourself. That seems like a good-enough compromise for me. |
I'm with Lasse. I agree that it's possible to write map patterns that look and act funny. But you can write all sorts of Dart code that looks and acts funny. If you do that, you get what you wrote. In particular:
Even a cursory inspection should reveal that this pattern is not what we'd call good code. So, yes, if you write bad code, you might get behavior you didn't expect. I think the best way to address that is to make the behavior simple and easy to predict instead of trying to figure out ways to prohibit writing bad code. Right now, the semantics for map patterns are pretty simple and work well:
Once a user knows those two points, they can reason correctly about a map pattern behaves even when it is strangely formed or uses unusual key types or a strange implementation of I do think it might be useful to have a lint that fires on map keys that "seem" to be equal but where we can't tell for certain because they don't use primitive equality. I'm not sure how feasible that is, but we could probably get it mostly working for doubles. |
Agreed—reporting duplicates for keys of type The broader issue about keys with a non-primitive operator
The point is that the length equality test has no other motivation than ascertaining that we are matching against every single key/value pair in this map, but that assumption is unsound in the case where It is possible that the non-primitive operator |
CC @pq since we're talking about possible lints |
Agreed. (And the reason we make map patterns default to checking the length unless you add
It's not provably true when you have non-primitive I think that's OK because the compiler isn't relying on any provable property of this for soundness. (For example, exhaustiveness checking doesn't care about map length.)
I don't think non-primitive |
It's unsound even if the The only thing that is "guaranteed" to work is identity (assuming that map lookup is at least consistent over time). We don't promise that If everybody behaves well, then In practice, I think the restriction that map pattern keys must be constant values will make almost every concern moot. switch (json) {
case {"kind": "foo", "foo": var f}: ...
case {"kind": "foo", "foo": var f, "optionalFoo": var fo}: ...
case {"kind": "bar", "bar1": var b1, "bar2": var b2}: ..
} For those, the map length check is essential. I don't want to explain why some keys behave differently, and require a We can still give warnings if you do have identical keys, or equal keys with primitive equality. But you can ignore those if you know what you are doing. (Which is why they should be analyzer warnings, so you can ignore them.) |
And, for what it's worth, a weird implementation of |
OK, so nobody else cares about misleading map key counts. But duplicate keys of type |
I would be in favor of an analyzer hint that flags duplicate map pattern keys. I've filed a feature request: dart-lang/sdk#50588. |
I'd still recommend a warning (from the analyzer) if two map pattern keys in the same map pattern
Since I'd be willing to make it a language error if two keys are identical, because I really cannot see any use for that. I won't agree to making it an error to be equal using primitive (Heaven forbid that someone makes an identity map mapping different NaN representations to something. At least I don't think we can create other constant NaN values than |
Thanks to @sgrekhov for bringing up this topic. Consider the following program:
The class
double
does not have primitive equality. This means that it is not a compile-time error to have two keys in a map pattern that are equal (basically, we say that this can't be known at compile-time, and that is indeed true in the general case). The run-time behavior is to check that the given scrutinee is a map with suitable type arguments (there are a few possible choices, but they don't matter here), and thelength
of the map matches the number of subpatterns, and that each subpattern matches.So the match succeeds (and binds
x
andy
to true).However, the 'success' is misleading: The
length
requirement certainly doesn't establish any useful guarantees about the scrutinee, and it is confusing (and non-obvious in the general case) that we're binding bothx
andy
to the value of the same key.A similar scenario can be created for any key type whose equality isn't primitive. In particular, we could use any key type which is a class
C
whereObject.==
has been overridden byC
or any of its superclasses. For example:This matching behavior seems confusing and bug-prone, so we might want to report a problem at some point. However, it is an undecidable problem in general whether or not there is a match, because it runs a developer-written function body.
Would it be better to raise a dynamic error in the case where multiple keys in a map pattern are identical or equal? (We can't make it an error to get the same value twice, because there is nothing wrong with
{1: true, 2: true}
).The 'correct' solution seems to be that we detect the situation where the k key constant expressions of a map pattern do not look up every distinct key of the scrutinee (even though the
length
equality test seems to imply that we're getting all of them). However, there's no easy and performant implementation of "looked up every distinct key of a map". Alternatively, we could check that the matching process obtains every member of thevalues.toSet()
of the given scrutinee (in the example above, the pattern matching process never retrieves the value0
), but that's of course an imprecise check because accidental key duplication could have caused us to omit some values that are duplicates (again{C(1): true, C(2): true}
could matchC(1)
twice and never matchC(2)
, and still get all values).@munificent, @stereotype441, @kallentu, @natebosch, @leafpetersen, @jakemac53, @lrhn, WDYT? Should we report a dynamic error because the "precise
length
required, but multiple matches of one key and some other keys ignored" situation is a likely bug? Alternatively, should we just let it pass (and perhaps lint against using map keys with non-primitive equality)?The text was updated successfully, but these errors were encountered: