400 likes | 658 Views
Variance and Generalized Constraints for C# Generics. Andrew Kennedy Microsoft Research Cambridge Joint work with Claudio Russo, Burak Emir, Dachuan Yu & Benjamin Pierce (work in progress on decidability). C# Generics FAQ.
E N D
Variance and Generalized Constraints for C# Generics Andrew KennedyMicrosoft Research Cambridge Joint work with Claudio Russo, Burak Emir, Dachuan Yu & Benjamin Pierce (work in progress on decidability)
C# Generics FAQ Q: Button is-a Control (“inheritance is subtyping”, in C#) List<Button> is-not-a List<Control> Why not? A: It’s unsound to assume that generic types behave “covariantly” with respect to subtyping. This talk: Add safe variance annotations to generic types. Generalize generic constraints to complement variance Look in detail at subtyping and subtype checking
A simple collection class class Cup<T> { Cup(T initial); void Replace(T contents); T Peek();}
abstract class Drink { } class Wine : Drink { DateTime GetVintage(); … } class Water : Drink { bool IsMineral(); … } Cup<T> is not covariant class Cup<T> { Cup(T initial); void Replace(T contents); T Peek();} To be safe for contravariance, a type parameter T must appear only in “consumer” positions in method signatures void Miracle() { Wine muscadet = FindInWineList(“muscadet”); Water evian = FindInWaterList(“evian”); Cup<Water> cw = new Cup<Water>(evian); Cup<Drink> cd = cw; cd.Replace(muscadet); Water water = cw.Peek(); bool b = water.IsMineral(); // GULP! To be safe for covariance, a type parameter T must appear only in “producer” positions in method signatures
We’ve known this a long time… William Cook. A Proposal for making Eiffel Type-safe. In S. Cook, editor, ECOOP'87 Proceedings, pages 57-70. Cambridge University Press, July 1989. “Statically type-correct Eiffel programs may produce run-time errors because (1) … (2) … and (3) two applications of a generic class are assumed to conform if the actual arguments conform. The third problem is solved by case analysis on the variance of generic parameters.”
Variance design space • Covariance everywhere: follow poor intuition and ignore or patch unsoundness. (Eiffel, Java/C# arrays) • Use-site: give control to generics client. (Java 1.5 wildcards) • Definition-site: put burden on generics provider. (NextGen, Scala) We adopt (3). Surprisingly, no previous work on • Proof of soundness for definition-style variance • Algorithmics of subtyping with Java-style inheritance
Use-site variance “On Variance-Based Subtyping for Parametric Types”A. Igarashi and M. Viroli, in Proceedings of ECOOP 2002 • Many standard classes use type parameter covariantly and contravariantly • So: let user decide, and filter methods according to use of type parameters in the signatures • Can invoke Peek on Cup<+Wine>, but not Replace • Can invoke Replace on Cup<-Wine>, but not Peek • Non-variant instantiation is subtype of variant instantiation:Cup<Wine> <: Cup<+Wine> <: Cup<+Drink>Cup<Drink> <: Cup<-Drink> <: Cup<-Wine>
Use-site variance, in Java1.5 • Java’s variant on variance is “wildcard types” • Cup<? extends Drink> is covariant • Cup<? super Drink> is contravariant • Can be interpreted as “bounded existential” types • Cup<? extends Drink> = 9(X<:Drink). Cup<X> • Cup<? super Drink> = 9(X:>Drink). Cup<X> • Use-site variance – and wildcards – put burden on user: • Annotations must be maintained on every use of a generic type • Types can get complicated, with nesting of variance annotations
Definition-site variance // From .NET Framework v2.0 interface IEnumerator<+T> { T Current { get; } } interface IEnumerable<+T> { IEnumerator<T> GetEnumerator(); } interface IComparer<-T> { int Compare(T x, T y); } interface IComparable<-T> { int CompareTo(T other); } delegate void Action<-T>(T obj); // From LINQ (C# 3.0) delegate U Func<-T,+U>(T arg); IEnumerator<Button> buttons = … IEnumerator<Control> asControls = buttons; Fix variance of type parameters up front No annotation necessary on uses
Easy explanation of variance • Covariance:IEnumerator<Button> <: IEnumerator<Control>An enumerator of Buttons also enumerates Controls.Obviously. • Contravariance:IComparer<Control> <: IComparer<Button>A comparer of Controls can also compare Buttons.Of course.
Factoring of interfaces • With definition-site variance, library designer will typically expose covariant and contravariant behaviour through different interfaces e.g.interface IListReader<+X> { public X Get(int index); public Sort(IComparer<X> comparer); …} interface IListWriter<-Y> { void Add(Y item); void AddRange(IEnumerable<Y> items); …} class List<Z> : IListReader<Z>, IList<Writer<Z> { private Z[] arr; public List() { … } public Z Get(int index) { … } …} Covariant interface Contravariant interface Non-variant class
Problem: variance isn’t enough // Immutable Set type; would like it to be covariant class Set<+T> {public Set();bool Exists(Predicate<T> p);bool Member(T t);Set<T> Union(Set<T> s); } OK, as T appears covariantly Not OK, as T appears contravariantly
Solution: generalized constraints // Immutable Set type; would like it to be covariant class Set<+T> {public Set();// This is OK bool Exists(Predicate<T> p);// This is OK bool Member<U>(U t) where T : U;Set<U> Union<U>(Set<U> s) where T : U; } • This pattern is applied in Scala (Odersky et al). Scala supports upper and lower bounds on type parameters. We observe that without such constraints: • Cannot give signatures to many functions on covariant collection types • Cannot write generic visitor pattern for variant types OK, as T appears covariantly
Generalized constraints, sans variance • Constraints can be localized to methods that take advantage of them (beyond upper/lower bounds): interface ICollection<T> { void Sort() where T : IComparable<T>; bool Contains(T item) where T : IEquatable<T>; • Can reduce equational constraint T=U to pair of subtype constraints T:U and U:T.class List<T> { List<U> Flatten<U>() where T=List<U>; …} Cannot express list-flatten signature without such a constraint! (See OOPSLA’05 paper on GADTs)
Easy explanation of constraints • Not even a new feature: rather, the removal of a restriction • In C# 2.0, in a constraint “where T : U”, type T must be • Either: a class type parameter, if constraint is declared on a class • Or: a method type parameter, if constraint is declared on a method • Simply remove this restriction!
Variance restrictions, by example class C<+T,-U> where T : D<T>, U : E<U> {private T f1; private U f2;public readonly T f3;public C(T x, U y) { … }public T m1(U x) where T : I where J : U { … } } No restrictions on private members, given right definiton for private! No restrictions on use of T and U in class constraints Read-only fields are covariant No restrictions on constructors Right of constraint is contra Argument is contravariant Left of constraint is covariant Result is covariant
Theory • Why? Misquoting…“There are two ways of constructing a type system. One way is to make it so simple that there are obviously no deficiencies. And the other way is to make it so complicated that there are no obvious deficiencies.”- C.A.R. Hoare We’re here
Formalizing subtyping • Formalization of subtyping typically follows two styles • Declarative: reflexivity + transitivity + system-specific rules • Nice for reasoning about meta-properties (e.g. closure under substitution) • Syntax-directed: syntactic shape of goal uniquely determines a rule to apply • Usually the first step towards deriving an algorithm • To show equivalence of two styles, hard part is usually showing that transitivity is “admissible” in the syntax-directed system.
Rules (1): reflexivity & transitivity • Principle of safe substitution: if T <: U (T is a subtype of U) then value of type T can safely be used wherever a value of type U is expected. • Rules for reflexivity and transitivity follow from this: T <: U U <: V T <: T T <: V
Rules (2): generic inheritance • Inheritance declarations induce subtypes (for simplicity just consider one type parameter): C<{+|-}X> : ...,V,... C<T> <: V[T/X] • This is just the standard rule from Java and C# 2.0. Here V[T/X] means “V with T substituted for each occurrence of X”.
Rules (3): variance • Covariant and contravariant subtyping: C<+X> T <: U C<-X> U <: T C<T> <: C<U> C<T> <: C<U> • Invariant subtyping: C<X> T <: U U <: T C<T> <: C<U>
Rules (4): constraints • Collect constraints from class and method into a set . • Now write ` T <: U to mean “T is subtype of U under assumptions in ” • Most rules just plumb constraint set around. Also add hypothesis rule: T <: U in ` T <: U
On one slide... ` T <: U ` U <: V (refl) ` T <: T (tran) ` T <: V C<{+|-}X> : ...,V,... T <: U in (base) (hyp) ` C<T> <: V[T/X] ` T <: U C<+X> ` T <: U C<-X> ` U <: T (co) (contra) ` C<T> <: C<U> ` C<T> <: C<U> C<X> ` T <: U ` U <: T (inv) ` C<T> <: C<U>
We’re not done... • Is this safe? Consider a valid use Foo<T,U>. To be valid it must satisfy constraints, so C<T> <: C<U>. But this can only be the case (inspect the rules) if T <: U. • So it’s sound to assume X <: Y in body of method above. • Likewise: • if C were contravariant we could derive Y <: X • if C were invariant we could derive X <: Y and Y <: X. This is the “decomposition” rule that we need for GADTs (cf Kennedy & Russo, OOPSLA’05) static Y Foo<X,Y>(X x) where C<X> : C<Y> { return x; }class C<+X> { ... }
Rules (5): inversion C<+X> or C<X> ` C<T> <: C<U> (inv-co) ` T <: U C<-X> or C<X> ` C<U> <: C<T> (inv-contra) ` T <: U C<vX> : …,D<W>,...` C<T> <: D<V> (inv-base) ` D<W[T/X]> ` D<V>
We’re still not done...? • What about X <: C<Y>, X <: C<Z> ` Y <: Z and X <: C<Y>, X <: C<Z> ` Z <: Y for an invariant class C. They’re not derivable but seem valid (X can only inherit once from C) • Or what about X <: C, X <:D ` X <: E if E is a superclass of both C and D. • Finally, X <: string ` string <: Xis not derivable but surely holds as string is “sealed” (cannot be subclassed).
How do we know we’re done? • Typically one defines “semantic entailment” ² T <: U as follows: for all closing substitutions S, if S() then S(T) <: S(U). • We need to be a bit more explicit. Write C;` T <: U to interpret subtypes in the context of a set of class declarations C. Now we define C; ² T <: U as follows:for all valid extensions D ¶ C and closing substitutions S, if D` S(D) then D` S(T) <: S(U). • Soundness: if C; ` T <: U then C; ² T <: UCompleteness: if C; ² T <: U then C; ` T <: U • Open question: complete set of rules for C# with variance?
Towards a subtyping algorithm • Declarative rules are highly non-deterministic e.g. to apply transitivity must pluck “middle” type from nowhere • Reformulate as syntax-directed rules: • Transitivity should be a consequence • Assumptions are simple bounds on type parameters (e.g. X <: T or T <: X), not general subtype constraints • Assumptions must first be “closed” under transitivity, decomposition and inheritance • Otherwise, we might have constraints X<:T and T<:Y but not be able to derive X<:Y, or constraints C<X> <: Y and Y <: C<Z> but not be able to derive X <: Z. • See Trifonov & Smith, and Pottier for similar ideas in ML subtyping.
Syntax-directed rules C<{+|-}X> : ...,V,... ` V[T/X] <: U (refl) ` X <: X (base) ` C<T> <: U X<:T 2 ` T <: K ` T <: U U<:X 2 (upper) (lower) ` X <: K ` T <: X C<+X> ` T <: U C<-X> ` U <: T (co) (contra) ` C<T> <: C<U> ` C<T> <: C<U> C<X> ` T <: U ` U <: T (inv) ` C<T> <: C<U>
Equivalence • Theorem (equivalence of decl. & syntax-directed subtyping)Suppose closed and consistent, ` and ` . Then ` T <: U iff ` T <: U. • Proof requires transitivity for syntax-directed system. (And even if we take syntax-directed rules as “definitive”, we still need transitivity for soundness of the type system). • Proof of this is tricky - not just a simple induction. Clue: transitivity relies on valid use of variant type parameters in inheritance declarations. Consider class N<-Y> class C<+X> : N<X>Now C<Button> <: C<Control> and C<Control> <: N<Control>but not C<Button> <: N<Control>.
A semi-algorithm • The syntax-directed rules can be interpreted as a subtype checker • If there is a derivation, the subtyping procedure will terminate with “yes” • Unfortunately, if there is no derivation, the subtyping procedure may fail to terminate. Considerclass N<-Y> { } class C : N<N<C>> { static N<C> Foo(C c) { return c; } } • To type-check this code, we need to check C <: N<C>:C <: N<C> → N<N<C>> <: N<C> (base)→ C <: N<C> (contra)→ …
Worse still... class N<-Y> { } class C<X> : N<N<C<C<X>>>> { static N<C<int>> Foo(C<int> c) { return c; } } • C makes use of “expansive” inheritance, namely inheritance that leads to unboundedly many instantiations. • The closure of { C<T> } under inheritance and decomposition is infinite and includes { Cm<T> | m ¸ 0 } ...sends the checker into a loop with types of ever-increasing size:C<int> <: N<C<int>> → N<N<C<C<int>>>> <: N<C<int>> (base)→ C<int> <: N<C<C<int>>> (contra)→ N<N<C<C<int>>>> <: N<C<C<int>>> (base)→ C<C<int>> <: N<C<C<int>> (contra)→ … → Cm<int> <: N<Cm<int>>
Undecidability of general case • Take ground subtyping: rules (base), (co), (contra) and (inv) • Basic restrictions on inheritance • No mixin inheritance (C<X> : X) • No cycles through “head” class (C : D : E : C) • Subtyping is undecidable. • Take an instance P of PCP (Post Correspondence Problem, known to be undecidable) • Reduce it to a corresponding subtyping problem C` T <: U • Theorem: P has a solution iff C` T <: U is derivable.
Sketch of reduction • PCP: given a set of pairs of words {(u1,v1), ..., (un,vn)} find sequence of indices i1,...,ir such that ui1...uir = vi1...vir • Reduce to subtype problem as follows. • Define class L<X> for each letter L in alphabet, and E for empty. Words can be encoded as nested instantiations e.g. abc is a<b<c<E>>>. • Define contravariant interfaces N<-X>, N0<-X>, N1<-X>, ..., Nn<-X>, and class S. • Define class C<X,Y>. We will “push” words from {u1, ..., un } onto X, and words from {v1, ..., vn } onto Y. Declare multiple supertypes of C:C<X,Y> : NN1C<u1X,v1Y>, N1NC<u1X,v1Y>, ... , NNnC<unX,vnY>, NnNC<unX,vnY>, NN0S<X>,N0NS<Y> • Given a goal C<A,B> <: N<C<A,B>>, can reach subgoal C<uiA, viB> <: N<C<uiA,viB>> (i.e. prepend the i’th pair of words), or reach subgoal S<A> <: S<B>, which is satisfied only if A=B (i.e. test whether we have a solution).
Decidable fragments Three interesting ingredients in reduction: • Contravariance (can be mimicked using wildcards in Java) • Expansive inheritance (e.g. C<X> : D<C<B<X>>>) • Multi-instantiation inheritance (e.g. C : I<int>, I<float>) Proof: reduction from PCP Proof: measure on subtype judgments Proof: algorithm explores finite space of types
Complexity • Subtyping derivations can be exponential in size of program. class N<-Y>class C0<X> : N<N<X>>class C1<X> : C0<C0<X>>...class Cn<X> : Cn-1<Cn-1<X>>static N<Cn<int>> Foo(Cn<N<int>> c) { return c; } • Derivation of Cn<N<int>> <: N<Cn<int>> uses 2n+1 instances of (contra).
Work in progress on decidability • If multi-instantiation inheritance is disallowed, then we can formulate subtyping so that subtype derivations are unique. • Now observe pattern of regress in our “expansive example”: C<int> <: N<C<int>> → N<N<C<C<int>>>> <: N<C<int>> → C<int> <: N<C<C<int>>> → N<N<C<C<int>>>> <: N<C<C<int>>> → C<C<int>> <: N<C<C<int>> → … → Cm<int> <: N<Cm<int>> • The <int> instantiation is irrelevant to validity. Moreover, at any point in the reduction, all but the first occurrence of C is irrelevant to validity. • (For some scenarios ) we can show that the size of the “relevant” part of the subtype assertion is bounded by a constant (computable for the given problem), even though the “irrelevant” part grows unboundedly. • Hence reduction will either terminate (yes or no), or repeat a state (up to relevance) at which point we return “no”.
Implementation of C# with variance and generalized constraints • Implemented (by Burak Emir) as diff to C# 2.0 codebase. • CLR 2.0 already supports variance on interfaces and delegates • No runtime support for generalized constraints, so we insert a checked cast whenever we know that the verifier will fail to validate a use of subtyping • Recently ported to shared-source release of C# 2.0. Will post on MSR download site sometime soon. • Continued engagement with product teams in Redmond(Variance is a frequently requested feature)