-
Notifications
You must be signed in to change notification settings - Fork 214
Operator == on reified types #821
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
My main worry of moving towards mutual subtyping for implementing equality, is that I'm not sure if we can implement it via canonicalization techniques anymore. Up until recently dart2js used to override Type.== and we are very excited that we no longer do. Overriding == meant that we couldn't properly support using type literals in switch statements and I believe we had other issues too. For reference, here are a few issues highlighting this problem: My 2c: I'd like to make sure that we continue to have the freedom to use canonicalized runtime types to implement equality (that is, to allow us to implement |
@lrhn and I just discussed this with @johnniwinther, and he also mentioned that if we wish to move to mutual subtyping we're at a point in time right now where we should get it done. It is not obvious how difficult this will be, but at least it wasn't obvious to @johnniwinther that it is unrealistic. So maybe we can do it.
Ideally, we could get this kind of speed and maintain that
Right, if all run-time entities representing reified types are canonicalized then type equality can be implemented as a simple pointer comparison. But I'm slightly surprised that there is no need for exceptions (that is, I thought that only most run-time entities representing types would be canonicalized, not all of them). Anyway, a pure mutual-subtype approach would actually allow us to use canonicalization, though based on a slightly more coarse-grained model of types: We take the Dart subtype graph and collapse all cycles (for instance, there is only one top type and it is not possible at run time to detect whether it was spelled (Of course, this applies to instances of We already have normalization which will collapse a lot of types that are mutual subtypes into a single representative, and The trade-off is that we will then have a rather coarse-grained model of types in terms of reifications of type In the future, with pure nnbd, we can eliminate the Note that reified types are often used for purposes that are somewhat similar to run-time reflection, e.g., to create a map where a type can be mapped into a function that somehow performs a task with respect to that type. I suspect that a clear and consistent model of types, albeit somewhat coarse-grained, will be at least as useful as a more fine-grained model where we claim that a list that will throw if we add null has the same type as |
Here's a bit more about the rationale. The approach that I'm now recommending (after a lot of discussion), and which is described here in more detail, could be roughly summarized as follows: Reified types ignore nnbd. This is because the mutual subtyping approach implies that we eliminate all cycles in the subtype graph in order to get a proper equivalence relation, and a relation which can be implemented using canonicalization. But this means that equality on reified types is a bit more coarse-grained than you would expect (and maybe more than you'd like ;-), in particular because it makes The main rationale for this is that (1) it is a mild transition from current code, so the semi-reflection-ish kind of code that exists and uses reified types (say, places where a We can use this approach forever, if we wish to do that. We can also choose to refine the equivalence when |
There are definitely a number of problematic complex cases that we might not be able to solve satisfactorily in all cases. Core RequirementI think that there is a core functionality that we should ensure that we do support. It mostly boils down to: new Foo().runtimeType == Foo That operation must work (as long as nobody overrides The The test should work whether the code occurs in a null-safe library or a legacy library. If the value of It should preferably work whether That does not say whether the type object of So, summary: the type objects of Composite typesThat leaves us with the issue of composite types. Is a type objects of a First remember that any two type objects which are equal must also have the same That's one of the primary arguments that have been used against using mutual subtype as defining And with that restriction, I think we should not be trying to do composite type equality in any clever way, we should just use "the same type" as rule, with "the same type" being defined as having the same normalization in the NNBD normalization rules ( That would allow a structural equality check traversing normalized type structures and allowing different top types to be equal. (I'd still recommend removing the distinction between SummaryIn summary I'd suggest:
That allows us to use the existing type normalization algorithm (which we'd do anyway), which allows a consistent It does mean that for The only other alternative is to what Erik suggested above, to remove all nullability information (and then normalize), but I don't think the later breaking change to reintroduce the difference is worth it, and I don't think not introducing the difference is going to be viable in the long run either. A non-transitive equality where the type objects of |
My impression is that regardless of what we do, this will be confusing, and thus the best thing we can do is be as simple as possible, meaning, have as little magic as possible. I would also posit the following principles:
Given that, I would say that there should be no magic runtimeTypes, and so If magic were to be introduced, I think I would limit it to the top-level *-stripping behaviour @lrhn describes above, such that the types would actually be |
This is a very long discussion about something that I thought was pretty well settled. I haven't read through all of the discussion in detail yet, since a number of the initial premises of the discussion seem wrong to me. Let me summarize my understanding of the situation, and then perhaps someone can (briefly!) summarize what, if any the remaining issues are? So I see two issues raised. The first is mutual subtyping, and the second is the treatment of Mutual subtypingFirst, mutual subtyping. I'm not sure that I understand the discussion around whether to use mutual subtyping as the criteria for equality, since I am proposing to use something (hopefully) equivalent to mutual subtyping. That is, the specification defines runtime type equality to be performed by comparing the normal forms of the types. In the final type system with no In the presence of Legacy type equalityAs specified, I have chosen to make runtime type equality equate So that's the summary of the existing specification and why the choices in it were made. Are there problems with the above that I'm missing? |
@leafpetersen I do think that the proposed equality satisfies my "core requirement", and the only difference is that it ignores My worry about performance would be that you need to traverse the type structure on every check for equality - even if you canonicalize all type objects (at least until the program is entirely null safe). That's where not ignoring That is assuming an implementation strategy (embedding the internal type representation directly in the I'm fine with either approach if the people implementing it are. |
I believe that the implementation strategy that we sketched on the board here for the web compilers was to cache two normalized forms: the NORM, and the * erased NORM. Am I remembering that correctly @fishythefish @sigmundch @rakudrama @nshahan ? |
I agree with @leafpetersen that this issue should have been settled by now. We raised concerns about type equality in November and type equality was redefined then. In the VM, we have implemented the nnbd specified type equality (and optimized it with intrinsics on all architectures). There is no noticeable slow down, although we cannot rely on hashes, since different hashes do not imply unequal types. Hashes reflect canonical equality. |
correct. Whenever type objects are returned, you get the normalized type objects. So |
Excellent. And if we don't print the |
I was worried about reified type equality in a transitional world (that is, where some However, as I also mentioned, the legacy-erasure approach would introduce the need to have a breaking change later on, in order to get to the model in the pure nnbd setting. So let's keep the rules as they are. My worries about migration haven't disappeared, of course, and I've written an example at the end of this comment to illustrate why migration will have some inconvenient elements with the current rules. A couple of remarks on the discussion: @Hixie was also worried about the transitional semantics:
As @leafpetersen noted, we will have approximately the mutual subtyping semantics (except that we still distinguish
That's good, and I had no doubts about that. @leafpetersen continues:
The transitional subtyping relation isn't a partial pre-order (that is, it isn't transitive), so I agree that it's unclear what it means to say that it corresponds to normalization. That's the reason why I proposed using legacy-erased types as the basis for reified type equality: This is basically transitional subtyping made transitive, and equating top types. This would make equality on reified types more coarse-grained, but it would give us some nice properties: The reified types would reflect a proper partial order (transitive, reflexive, anti-symmetric), and the story about which reified types are equal (across a program with some nullsafe and some legacy libraries) would be very simple: Legacy-erase, considering top types as equal, then check. Of course, if reified types are always normalized in this sense then About the question:
With the specified rules, it will be necessary in nullsafe code to compensate for the kind of type inequality that I was worried about: Null-safe code will need to manually erase nnbd types when they will be reified and tested for equality. Here is an example. Assume that we have a piece of legacy code where a function // Library L before migration.
List<int> f(int i) => [i, null];
typedef List<int> fType(int i);
Map<Type, Function> map = { fType: f };
void main() {
fType g = f;
print("Call via lookup: ${map[fType](42)}. Directly: ${g(42)}");
} When we migrate this library, we need to introduce a distinction between the regular type (here: // Library L after migration.
List<int?> f(int? i) => [i, null];
// Actual type of `f`.
typedef List<int?> fTypeMigrated(int? i);
// "Additional type of `f`", needed for equality tests.
typedef List<int> fTypeForTypeEquality(int i);
Map<Type, Function> map = {
// Use the additional type when reified and tested for equality.
fTypeForTypeEquality: f,
};
void main() {
// Use the actual type of `f` when it is used as a type.
fTypeMigrated g = f;
// Use the additional type for lookups (again: reified and tested for equality).
print("Call via lookup: ${map[fTypeForTypeEquality](42)}. "
"Directly: ${g(42)}");
} In the migrated code, it is a compile-time error to change the type of But we also need So my response to the question about whether we should be worried would be as follows: I expect migration of code using reified types (for instance: using maps from Each location in client code where said type is mentioned would need to use one or the other (only one of them will work). The choice depends on whether it will be used as a type, or it will be reified and used for equality tests. This may or may not be worrying. In any case, I wasn't aware that implementation efforts occurred already in November 2019. The conclusion today must be that we leave the rules unchanged. |
Uh oh!
There was an error while loading. Please reload this page.
The nnbd spec has new rules for the result returned by
operator ==
in the case where it compares two instances of typeType
which were obtained as reified representations of Dart types.In short, they consider the underlying types, normalize them, erase
*
, and then compare syntactically.For example,
Object*
compares equal toObject
, but not equal toObject?
.However, it is not unreasonable to claim that the properties of
Object*
resemble those ofObject?
more closely than those ofObject
. In other words, it seems to contradict reasonable expectations thatoperator ==
treatsObject*
in this way.Of course, the dynamic type of an instance never has a
*
at the top level, so these conflicts arise mainly for type arguments, and for parameter/return types of function types.Cf. also #818, where this kind of conflict comes up in the special case of (a)
X* Function<X extends Object*>(X*)
vs. (b1)X Function<X extends Object>(X)
respectively (b2)X Function<X extends Object?>(X)
. Again, (a) compares equal to (b1) and unequal to (b2), but the behavior of (a) more closely resembles (b2) than (b1).In general, these new rules will always consider legacy types to be equal to some nnbd types where
?
does not occur, and unequal to the same types where?
does occur, even though the latter are, arguably, more reasonable candidates for being considered equal.Can we do something about this? Isn't it both confusing and impractical that the legacy
Object*
type is equal to the nnbdObject
type and unequal toObject?
, but it's much more like the latter (and the same holds for anyT*
,T
, andT?
)?The currently specified approach uses mutual subtyping, that is: If
t1
andt2
are reifications of the typesT1
andT2
thent1 == t2
evaluates to true iffT1 <: T2
andT2 <: T1
. The language team agreed on this, but we have held back on getting it implemented because of worries about breakage.We could do it now, as part of the switch to NNBD. One nice property of the mutual-subtyping approach is that it is consistently aligned with assignability (that is, if the run-time type of an object is equal to the type annotation of a variable then it is safe to assign that object to that variable).
@leafpetersen, @munificent, @lrhn, WDYT?
The text was updated successfully, but these errors were encountered: