-
Notifications
You must be signed in to change notification settings - Fork 212
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
Empty Top Types. #2756
Comments
It's actually equality (operator It's true that some of these members are somewhat misplaced in some situations: If we say that it is pragmatically useful to have at least some members on Which problems does it create that there is no compile-time error for |
One instance where this comes up a lot is when I try to generalize some piece of code. E.g. consider a foo that takes a String and consider a situation where we'd like to generalize and take a sequence-of-something instead. i.e. we'd like to go from: void foo(String a) {
// ...
print(a.hashCode);
// ...
} to void foo<E>(List<E> a) {
// ...
print(a.hashCode);
// ...
} String has an equality that doesn't at all behave like the List equality. It would make little sense to use the equality that comes with the new type here. I'd like to be able to use the static type system to prove that I'm not using the default equality of the type that I'm migrating to. For small examples this is trivial to verify manually, but it is hard to do that with bigger examples because the equality may leak in various places such as when the previous type has been used in e.g. a Set. void foo<E>(List<E> a) {
final b = a;
final c = b;
// ...
print(a.hashCode);
// ...
final d = {c};
// ...
} The type that we are migrating to might not even have a sensible equality. Unfortunately, today I can't communicate that to the type system so I've been using the following mixin instead: mixin ForbidEqHash {
@override
bool operator ==(
final Object other,
) {
throw Exception(
"Cannot use .== on " +
runtimeType.toString(),
);
}
@override
int get hashCode {
throw Exception(
"Cannot use .hashCode on " +
runtimeType.toString(),
);
}
} Edit: In the example above, I'd swap String with the hypothetical "Top" type first to let the analyzer tell me whether any of its Object members are being used (or I'd use a custom interface implementing Top instead of using List.)
Did it come across like I was implying that? I actually have no issues with Object containing any of the methods that it does. Being general comes with a lot of overhead that is not worth it in many cases and I think Dart balances that very well. |
Too bad using But in seriousness, I don't see much value in being able to pretend that a type doesn't have members that all objects do have. It might be occasionally useful, but unlikely to be worth the implementation effort, maybe not even in the absolute, but definitely not in opportunity cost. |
I see, you are referring to the following; void main() {
bar(0);
}
void bar<T extends foo>(T a) {
print(a == a); // true
}
typedef foo = void; Repurposing void for this would be wonderful.
Perhaps I am misunderstanding what you are saying, or I wasn't clear enough, but another way how the Top type could be used is as an alternative to the default Object superclass. Consider the following: class Bar implements Top {
final int i;
const Bar(this.i);
} We wouldn't be pretending that Bar doesn't have those members, it actually wouldn't have those members because it wouldn't be of type Object and
The answer to this question would be no if String interpolations are syntax sugar over calling the toString() of a value of type Object, using x in an interpolation would not be possible because the type of x has no supertype of type Object.
Thank you, I appreciate the honesty. |
@modulovalue wrote:
OK, thanks for the careful response!
void foo(String a) {
// ...
print(a.hashCode);
// ...
}
void foo<E>(List<E> a) {
// ...
print(a.hashCode);
// ...
} I get the impression that we're talking about concepts like parametricity (a la Theorems for free), where the core point is that a generic entity (say, a generic function) treats entities whose type is a type parameter However, Dart does not support parametricity. One counterexample is that it is possible to evaluate Next, not even parametricity would suffice to make a transformation like the parameter type change from So I don't think it's a goal for Dart to be able to provide any particular guarantees about the before-and-after properties of code where this kind of change is performed. You'd simply have to check manually that the change yields code which is still plausibly error-free, based on whatever constraints you have on the expected behavior of Drilling down, we have the issue about using This issue would come up for any member access With unrelated declarations there's basically no hope that the before-and-after situation would be related, and we would definitely just have to study (and possibly debug) the code from scratch. With the related declarations we ought to have a safe situation, because both the declaration of @lrhn mentioned In particular, void foo<T>(T a) {
void safeA = a;
... // Same body as before, but using `safeA` rather than `a` when possible.
} The exception (where we cannot use T foo<T>(T a) {
void safeA = a;
... // Same body as before, using `safeA`. Except things like:
return a; // Can't use `return safeA;`, and `return safeA as T;` is silly.
} I have proposed introducing the notion of a void type (with the "don't use this object" constraint) associated with an arbitrary different type: T foo<T>(void<T> a) {
// Same body as before.
...
print('$a, ${a.hashCode}, ${a.toString()}'); // Compile-time errors now.
Object? o = a; // Compile-time error.
Object? o2 = a as Object?; // We have always allowed for `as` as an escape.
...
return a; // OK: Type `void<T>` is assignable to type `T`.
} The point is the same as with the current However, that proposal didn't generate much enthusiasm. Also, it hasn't been fleshed out, (e.g, how do we treat |
If Would Is An object which is not an |
Thank you, this captures a much broader idea that also includes the one that I am referring to and I agree with your conclusions. But it looks at it from a different angle and I don't think that what I'm referring to applies to this more general idea because of the following:
I agree with you. I meant to say that I'd like to have the type checker be able to prove the absence of the members that come with Object, and not to directly prove a new property e.g. that two declarations are safe in some way. Consider the following: abstract class Cat {}
abstract class Dog implements Cat {} Assume that we have inherited this code. Obviously a Dog is not a Cat, but the previous developer didn't know that. We can 'tell' the type system that a Dog is not a cat by removing the constraint that the Dog is a Cat. i.e. abstract class Cat {}
abstract class Dog {} If we assume that our program doesn't use any unsafe features (is, as, covariant & dynamic) and If we assume that the Dart type system is sound, then the type checker will prove that Dog is not a Cat by not allowing code that assumes that to be the case to compile. (Or rather, we have to provide a proof and the type checker will prove that our proof is correct) A more realistic example: abstract class BinaryTree<T> {
T get value;
BinaryTree<T> get left;
BinaryTree<T> get right;
} Conceptually, a BinaryTree is not an Object. It does not have a single sensible equality (we could compare the shape of two binary trees, we could compare the value, we could compare both, we could look at whether they are Today, we can tell the type system that a Dog is not a Cat and use the analyzer to help us fix our wrong assumptions, but we can't tell the type system that a BinaryTree is not an Object to let the analyzer help us fix our wrong assumptions. A hypothetical Top type would allow us to do that i.e. to (soundly, (that's the most important part to me here)) prove the absence of the members that Object comes with.
This could be helpful in solving the issue that I have raised, but I see a usability related issue with this idea (as a solution to the problem discussed here). Adding such a constraint would result in a plethora of unrelated errors. (i.e. 'don't use x... here, don't use x.hashCode here, don't use x... here, ...'). The errors that a Top type would produce would only be about the use of members that exist on Object.
Making 'Top' a supertype of dynamic would be in my opinion too invasive. Developers that don't want to use a type system can ignore the type system and use dynamic. I wouldn't want to remove this feature. I agree with dynamic being able to hold values of type Top and throwing directly as a solution to that problem.
Wouldn't these places already have to consider dynamic and be covered by that if Top is a subtype of dynamic?
I should have mentioned that I do not want this to be the case. This would break a lot of code and I think the default behavior of Object? being the default bound of a type parameter is a good compromise.
I'll have to think more about the potential consequences of this decision. 'Top' is probably a misnomer. If dynamic would be a supertype of 'Top' then Top is obviously not the Top Type. Calling it Top would be confusing. I'd like to propose |
Right, (except for the fact that we never got around to spell out the details): if As usual ;-), we drilled some holes in this armor, so there's a whitelist of things you can do, and the most significant one is The typical use case is actually the following: class A {
bool foo() { /* do the foo thing and then return 'true' if successful, otherwise return 'false' */ }
}
class B implements A {
void<bool> foo() { /* do the foo thing, which in this case is guaranteed to succeed*/ return true; }
} The point is that it is unnecessary to check the returned value in the case where the receiver has type The ability to complain about member accesses is just accidentally relevant here. |
Quote from Dart 2.0 Static and Runtime Subtyping
Lasse wrote:
Given that dynamic and void are both considered to be equivalent types, I'd say the answer should be no. I described in my previous response why I think |
Well, they are subtypes of each other, which means that they occupy the same equivalence class according to the subtype relationship. In particular, exactly the same objects will yield true when evaluating However, the static analysis of an expression of type You may have various philosophical takes on what it means to be a type, but we can at least say that the static analysis of Dart doesn't ignore these distinctions. So from that perspective all those top types can be considered to be the same type or not, depending on the purpose of the discussion. But if we add a new |
If you are saying that my use of the idea of the top type was not entirely correct here, then I agree. I did not mean to treat Object? as something different from what it is. This was a mistake on my part and I'd like to apologize. My proposal to call this new type I hope that the following diagrams are a more accurate representation of my intent: i.e. this is an incomplete model of the current type system. i'd like to propose the following: Where
Could |
Yes, and I'd prefer that we had a name for the top element, say
With a regular name for a regular class, (With the current approach it is a pure but convenient accident that The second subtype graph doesn't eliminate that anomaly, assuming that I still think your scenario would be served just fine by being able to specify that any given members of a given type cannot be invoked, and |
This sounds awesome because being able to think of those types in that way would make Dart much easier to teach and learn i.e. make Dart even better at what it already is very good at.
That may be the case, but what worries me about a solution like that is that it seems like this would be a new idea that new and existing users would have little intuition for. By having a type/interface that a user can assume not to carry ==, hashCode, and .toString(), a user can use pre-existing knowledge to reason about his code. If he wanted them, he could add them, like with other interfaces. If he didn't want them, he could not depend on Object, like with other interfaces. Other languages like e.g. Swift have special interfaces for these behaviors e.g. Equatable, Hashable, CustomStringConvertible (It's more complicated because there's also e.g. AnyHashable.) I'm not suggesting that Dart should adopt Swifts approach and be as granular as Swift is, but I do oftentimes miss that.
I will have to think more about that. (Yes, I intended for Always to have no other members except for perhaps runtimeType and noSuchMethod, but I wasn't completely clear about that.) |
@eernstg I had some time to think more about your Your approach seems to be much more expressive and could be made to 'feel' like my proposal without some of its downsides. I think that solving this particular issue with the solution that I proposed is not necessarily the best approach. Thank you Erik and @lrhn for your feedback and valuable input. I think I will come back to this in the future. |
Consider the following:
This code contains a top level function
foo
with a single type parameterT
. It might look likeT
doesn't have any behavior at all, butT
is implicitly declared to extendObject?
. Any value of type T comes with everything that comes withObject
e.g. equality, hashCode, an identity, runtimeType and much much more.It could be argued that this is bad, but for purposes of practicality this seems to be a very good trade-off and I'm not arguing against that. However, I'd like to be able to specify and have the type checker statically prove that
T
does not need to be anything as capable asObject
is.This idea of a type not being an Object could seem to make no sense because everything in Dart is an Object. However, if we think of
foo
as a specification and not as a program, T is simply overspecified. None of itsObject
capabilities are being used in foo. They are redundant and not necessary.Also, any reader of foo must for themselves first prove that e.g. the equality/hashCode/runtimeType/toString/... of
a
is used/unused before he can assume that to be a fact when reasoning about foo. An empty top type would make cases where that is needed much less painful.What I'd like to be able to do is something like the following:
where Top is e.g.
i.e. bound T to a Type that has no methods or other properties and is not an Object.
One interesting consequence of that is that it would mean that
T
could not be used as a key in Maps and could not be the value of Sets. Unfortunately, I think that Lists could also not contain values of type Top because they need equality for e.g.Iterable.contains
. But I claim that that is not a bad thing, but a useful feature. We would just need to be explicit about using Lists or Maps with T by e.g. injecting the construction of a List with T and using that instead.The text was updated successfully, but these errors were encountered: