-
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
Declaration-site Invariance #214
Comments
I would love better static control over variance in Dart. I think dealing with the historical baggage of It might be too much of a breaking change to make |
@munificent wrote
Indeed! But that could be addressed using use-site invariance (I'm working on that ;-). Kotlin's type projections are very similar to Java wildcards, and those two mechanisms have exactly the same meaning when it comes to the 'projection' part (it prevents access to members whose signature have a contravariant occurrence of a covariant type parameter, and vice versa). I don't immediately see any difference between type arguments But I also think it makes sense to allow developers to mark a type parameter as invariant in some cases. For instance, when a type parameter is used in a contravariant position in the type of a mutable field (that's the prototypical example of a "contravariant member"). Such a class is never going to work well with a covariant treatment of that type parameter, so it makes sense to allow the author of this class to say, once and for all, that this parameter should never be covariant. |
Yes, I totally agree. I think declaration-site variance is what you want 90% of the time. Generic class authors are much more likely than class users to have the sophisticated type expertise to know which variance is appropriate for the type. I just wanted to point out that declaration site variance isn't enough if we want to be able to eliminate the runtime overhead of covariance checks for most code, since we can't break List. |
The topic of unsoundness derived from the combination of covariance (for class type arguments) and contravariant placement of the corresponding type parameters (in the body of the class) has been discussed many times. One example is dart-lang/linter#1199. Another one, dart-lang/site-www#1017, contains further links to more than a dozen SDK issues on topics that are related to this issue. |
About the relationship between Java wildcards and Kotlin type projections: Ross Tate notes here that Kotlin adopted 'mixed-site' variance, which is basically a declaration-site plus use-site mechanism. The point is that declaration-site variance is too restricted to work well in some situations (in particular, in a language with support for mutable state), and use-site variance is (1) too verbose and (2) verbose/complex in client code, rather than in the declaration of the types that are used in client code. A similar idea was proposed by John Altidor et al. a couple of years earlier (here), where they add declaration-site variance to Java (which already has use-site variance under the name 'wildcarded types'). With support for both kinds, we avoid the verbosity and complexity wherever possible (declaration-site variance works like a default, that we then don't have to write in a lot of places), but we can still have use-site variance in those cases where declaration-site variance can't help us. So I'm working under the assumption that it makes sense for Dart to have both, too. |
It's true that we could express a larger variety of relationships using equations, and class C<X, Y> where List<X> extends Y {} But I think that's a quite different topic than declaration-site invariance: A richer set of constraints on the allowable values of type parameters will make it possible to get more guarantees in the body of the class that carries these constraints, but it would probably not be easy to prove that many new facts about usages (that is, in code where the type In contrast, declaration-site invariance will provide clients with new guarantees (e.g., because many method invocations which would otherwise be unsafe are now safe); but it makes no difference whatsoever for the body of the class that clients will only be able to have invariant references to it. So "where" clauses might be a useful topic to explore, but I don't think it's this issue. ;-) |
I haven't been doing "read it aloud" exercises consistently, even though I do recognize that it's a very useful property of language design if a straightforward reading provides relevant insight. But, for starters, I'd go with an obvious "Here's a class. Its name is The underlying notion of being an invariant type parameter is a property of the enclosing class: You can't obtain a related type by changing the value of the type argument. The other side of that coin is that if you have a variable of type Because of this connection, I've often used |
@tatumizer wrote:
Sepuling is a fine art! ;-)
List<invariant num> nums=[0];
var objects = nums as List<Object>; // static error?
var ints = nums as List<int>; // static error? With the given declaration of The things that are possible will all allow for the cast to The cast to (We need to specify exact types, cf. dart-lang/sdk#33307, and this particular question will be settled as part of that effort.) |
@tatumizer wrote:
Every language proposal which gets into the language is part of the development of Dart, so there is always a larger effort around these things. But I was just saying that, specifically, it's part of the topic 'exact types' to decide whether or not you'll get an error at the line About the Schrödinger state: With |
Closing this issue in favor of sound declaration site variance (#524). |
In response to #213, this issue is a proposal for adding declaration-site invariance to Dart.
Declaration-site variance is known from languages like Scala (using
+
and-
) and C# (using out and in), and the general idea is that each type parameter of a parameterized type declaration may have an associated variance.For instance,
class Foo[+A] ..
declares a Scala class whose type argumentA
is covariant, which means thatFoo[T2]
is a subtype ofFoo[T1]
wheneverT2
is a subtype ofT1
.In Dart, type parameters of generic classes are always covariant, which is unsound for certain usages (cf. #213). This means that it makes no sense to add variance annotations to Dart exactly as they occur in other languages, but we can add an annotation that eliminates the covariance which is otherwise applied to every type parameter. This is the motivation for using the name 'declaration-site invariance' for this feature.
This proposal uses the modifier
invariant
to indicate that a given type parameter is declared to be invariant, but it is of course possible to give this marker many other syntactic forms.This is particularly helpful in the case where the type argument is used in a contravariant position in the body of the generic class.
For instance, if the Dart
List
class had been declared asclass List<invariant E> ..
then theadd
method on lists would have been statically safe. In return, Dart developers would then have to refactor their programs such that they'd never need covariance for lists (so aList<int>
could never be the value of a variable of typeList<dynamic>
orList<num>
, only aList<int>
variable could refer to such an instance—plus of course variables whose type is a supertype ofList<int>
in a non-variant way, e.g., a variable of typeObject
would do as well).However, the ability to declare a type argument as invariant would be even more helpful in cases where the design of a class relies on contravariant usages of a type parameter in more complex locations. Here is an example:
The
Callbacks
class has fields whose types are contravariant in a type parameter, and ifX
is covariant then this amounts to a violation of the 'expression soundness' property. First consider the situation today:The initialization of
f
tocb.forOne
may seem to be type safe (replacingX
bynum
in the declaration offorOne
), but it is unsafe, and a dynamic check will be generated. Developers have no way to make this safe. The other side of this coin is that the assignment toforMany
is safe.In contrast, consider the situation where it is possible to declare the type argument of
Callbacks
as invariant:With invariance, developers can make the choice to have less flexibility (they can't abstract over the exact type argument), in return for improved static safety. Also note that the assignment to
cb.forMany
still works, becauseareEven
is actually a function whose type is a subtype of the required type, so there is no need to complain even when we know the truth. ;-)In summary, declaration site invariance in Dart would allow developers to make a different trade-off between the unsound but convenient ability to use covariance, and the more restrictive but sound use of invariance, for each type parameter.
Grammar
Syntactically, this feature consists of one extra modifier which can be added to the declaration of a type parameter. Here is the needed grammar update:
Static Analysis
The subtype rule that deals with variance is updated such that a parameterized type which is an instantiation of a generic class, C<T1, .., Ts>, is a subtype of another parameterized type C<U1, .., Us> whenever (1) Tj <: Uj for each j, and (2) Uj <: Tj for each j where the corresponding formal type parameter of C is invariant.
The rules governing class declarations are updated: Let C be a generic class with type parameter declarations invariant? X1 extends B1, .., invariant? Xs extends Bs and let D<T1, .., Tt> be a direct superinterface of C. It is a compile-time error if there is a j such that invariant? in the declaration of Xj is empty (that is, Xj is covariant), and there is an i such that Xj occurs in Ti, and the declaration of i'th type parameter of D is invariant (that is, this declaration contains the modifier
invariant
). Moreover, it is a compile-time error if there is a j such that invariant? in the declaration of Xj is present (that is, Xj is invariant), and there is an i such that Xj occurs in Ti, and the declaration of i'th type parameter of D is not invariant (that is, this declaration does not contain the modifierinvariant
).If we do not have the first of these errors then we could have the following kind of unsafe upcast sequences:
When
d
is initialized, the heap invariant ('soundness') is violated. Further usages such asd.x = 4.2
inmain
must fail because4.2
is not anint
, but, given thatX
ofD
is declared invariant, the static analysis would imply thatd.x = 4.2
is safe. Hence, we make the declaration ofX
inC
an error.Similarly, if we do not have the second of these errors then we could have the following kind run-time failure:
Even though the initialization of
d1
is an upcast (so it ought to be perfectly safe), we do not have the required subtype relationship fromC<int>
toD<num>
, so that initialization must cause a dynamic error, or the heap invariant (soundness) will be violated. So we make the declaration ofX
inD
an error.Discussion
Obviously,
invariant
is a verbose syntax for specifying that a given type parameter is invariant, and it is very likely that some other syntax will be chosen because it's more concise. On the other hand,invariant
matches the use ofcovariant
in Dart already, and it seems rather descriptive.For a less verbose syntax, using
=
(as inclass C<=X>
) would fit pretty well into the tradition of using+
and-
for covariant resp. contravariant type parameters (in research papers since the 90'ties, and in Scala), even though invariance is traditionally implicit, so there is no actual history of using=
for that. A counterargument could be that the occurrence of<=
at the given point is visually confusing, and maybe things likeclass C<X, =Y>
are, too.Following the syntax of C#, we could also use
inout
to indicate invariance. A counterargument against this choice would be that there are noin
orout
keywords for related declarations (soinout
doesn't come up naturally, and it's not a very natural "word" anyway). Moreover,inout
is perhaps already more strongly associated with value parameters (e.g., in Swift).It would also be possible to associate the invariance with the enclosing class as a whole (which means that it would be applied to all type parameters). This would be more concise when that effect is desired, but it is rather inflexible. It could be considered as an abbreviation.
Another issue which comes up naturally is why this proposal does not mention contravariance. It wouldn't be difficult at all to add support for
contravariant
as a modifier on a declaration of a formal type parameter (or some other syntax meaning the same thing), and then adjusting the variance related subtype rule accordingly. The reason why I did not include contravariance is simply that it introduces yet another bunch of rules around the subtype relationships of Dart, and this is something that developers will need to be aware of all the time. So we could easily do that, but we might also go for the simpler model without contravariance for classes, and relying on the current advice: "if you need contravariance, you should consider using a function type".One property of declaration-site invariance which may be somewhat inconvenient is that it propagates up and down the type hierarchy: Whenever there is a connection in the superinterface graph (directly or indirectly) between two type variables
X
of a classC
andY
of a classD
(e.g.,class C<X> implements D<int, Map<String, X>> {}
andclass D<Z, Y> {}
), we cannot make just one of them invariant—it must be none of them, or both of them; in general, for a set of thus "connected" type variables, we must make none of them invariant, or all of them.Consequently, declaration-site invariance can be used freely for newly designed class hierarchies, and for type parameters that are introduced in a class without being passed on to superinterfaces, but it will be less easy to sprinkle declaration-site invariance into existing class hierarchies for type variables that are used to specify any superinterfaces.
The text was updated successfully, but these errors were encountered: