Introduction 2 Understanding GADTs Why do we want GADTs? GADTs in functional languages Generalizing datatypes 3 GADT in C# Object-Oriented features and GADTs Limits and extensions 4 Backup slides Simple temperature example C# Idiosyncrasies Matthieu Dubet GADT and Object-Oriented Programming
Data Type GADTs have been introduced in 2003 independently as : First-Class Phantom Types by Cheney & Hinze Guarded Recursive Datatype Constructor by Xi et al. Generalize the notion of parametric algebraic datatypes in functional programming languages such as ML and Haskell. Matthieu Dubet GADT and Object-Oriented Programming
Object-Oriented Programming Andrew Kennedy, Claudio V. Russo Microsoft Research Ltd., Cambridge, UK OOPSLA ’05 Discusses the relation between GADTs and object-oriented programming The interested of GADTs-manipulating programs How to express GADTs in object-oriented programming languages such as Java or C# and their limitations Propose some extensions for those languages to remove those limitations Matthieu Dubet GADT and Object-Oriented Programming
we want GADTs? GADTs in functional languages Generalizing datatypes Plan 1 Introduction 2 Understanding GADTs Why do we want GADTs? GADTs in functional languages Generalizing datatypes 3 GADT in C# 4 Backup slides Matthieu Dubet GADT and Object-Oriented Programming
we want GADTs? GADTs in functional languages Generalizing datatypes An unconvenient problem Let’s implement a little language using an OOPL (C# here) Based on the well-knowed Interpreter design pattern from GoF Interesting points in this implementation: The virtual method Eval() which is responsible for the actual recursive interpretation The ugly but mandatory object return value The ugly but mandatory (int) casts Matthieu Dubet GADT and Object-Oriented Programming
we want GADTs? GADTs in functional languages Generalizing datatypes public abstract class Exp { public abstract object Eval(); } public class Lit : Exp { int value; public Lit (int value) { this.value = value ; } public override object Eval() { return value ; } } public class Bool : Exp { bool value ; public Bool ( bool value) { this.value = value ; } public override object Eval() { return value ; } } public class Plus : Exp { Exp e1,e2 ; public Plus ( Exp e1, Exp e2) { this.e1 = e1; this.e2 = e2 ; } public override object Eval() { return (int)e1.Eval() + (int)e2.Eval(); } } Matthieu Dubet GADT and Object-Oriented Programming
we want GADTs? GADTs in functional languages Generalizing datatypes class Untyped { static void Main () { Exp exp = new Plus( new Lit(1), new Lit(2)) ; System.Console.WriteLine(exp.Eval()) ; Exp error = new Plus ( new Lit(1), new Bool(true)) ; System.Console.WriteLine(error.Eval()) ; } } The compiler runs fine, but the execution doesn’t go that well: 3 Unhandled Exception: System.InvalidCastException: ... We don’t want our compiler to crash when we construct an incorrect AST. Let’s correct that. Matthieu Dubet GADT and Object-Oriented Programming
we want GADTs? GADTs in functional languages Generalizing datatypes Question 1: How to avoid the crash? Matthieu Dubet GADT and Object-Oriented Programming
we want GADTs? GADTs in functional languages Generalizing datatypes Question 1: How to avoid the crash? public override object Eval() { if (! (e1.Eval() is int) || ! (e2.Eval() is int) ) { System.Console.WriteLine( "invalid arg : not an integer"); return null ; } return (int)e1.Eval() + (int)e2.Eval(); } Matthieu Dubet GADT and Object-Oriented Programming
we want GADTs? GADTs in functional languages Generalizing datatypes Question 1: How to avoid the crash? public override object Eval() { if (! (e1.Eval() is int) || ! (e2.Eval() is int) ) { System.Console.WriteLine( "invalid arg : not an integer"); return null ; } return (int)e1.Eval() + (int)e2.Eval(); } The result: 3 invalid arg : not an integer Matthieu Dubet GADT and Object-Oriented Programming
we want GADTs? GADTs in functional languages Generalizing datatypes Question 1: How to avoid the crash? public override object Eval() { if (! (e1.Eval() is int) || ! (e2.Eval() is int) ) { System.Console.WriteLine( "invalid arg : not an integer"); return null ; } return (int)e1.Eval() + (int)e2.Eval(); } The result: 3 invalid arg : not an integer It’s a little better, but ... Matthieu Dubet GADT and Object-Oriented Programming
we want GADTs? GADTs in functional languages Generalizing datatypes Question 2: Why a runtime error is not good enough? Matthieu Dubet GADT and Object-Oriented Programming
we want GADTs? GADTs in functional languages Generalizing datatypes Question 2: Why a runtime error is not good enough? This a more open question. Generally, strong static typing prevents developers from huge amount of potential bugs int their source code. Theorically, a completely typed program can be proven right, thanks to the Curry-Howard correspondance. For an example of a real world type error which could have been avoided thanks to strong functional typing, see the backup slides. Matthieu Dubet GADT and Object-Oriented Programming
we want GADTs? GADTs in functional languages Generalizing datatypes An OCaml illustration We gonna try the same evaluator, without GADT: type exp = | Lit of int | Bool of bool | Add of exp * exp let rec eval a = match (a) with | (Lit l) -> Lit l | (Bool b) -> Bool b | Add(v1,v2) -> match (eval v1) with | (Lit val1) -> match (eval v2) with | (Lit val2) -> Lit (val1+val2) Matthieu Dubet GADT and Object-Oriented Programming
we want GADTs? GADTs in functional languages Generalizing datatypes The compiler compiles, but contrary to the C# implementation, it warns us about a potential mistake. Warning 8: this pattern-matching is not exhaustive. Here is an example of a value that is not matched: (Bool _|Add (_, _)) If we try to construct an incorrect AST, the program crashes on the eval v instruction. let v = Add( (Lit 1), (Bool true)) ;; eval v ;; Result: Exception: Match_failure ("", 11, 32). Matthieu Dubet GADT and Object-Oriented Programming
we want GADTs? GADTs in functional languages Generalizing datatypes GADTs to the rescue type _ exp = | Lit : int -> int exp | Bool : bool -> bool exp | Add : int exp * int exp -> int exp Thanks to GADTs, we explicitely tell the compiler that the type constructor Add expects its argument to be of type int exp, not the parametric one ’a exp. let rec eval : type a . a exp -> a = function | Lit x -> x | Bool b -> b | Add(v1,v2) -> (eval v1) + (eval v2) The eval function is now shorter, stricter and closer to the expected semantic. Matthieu Dubet GADT and Object-Oriented Programming
we want GADTs? GADTs in functional languages Generalizing datatypes Datatypes can be generalized in three ways: 1 The restriction that constructors all return generic instances of the datatype can be removed. This it the defining feature of a GADT. 2 The regularity restriction can be removed, permitting datatypes to be used at different types within their own definition. 3 A constructor can be allowed to mention additional type variables that may appear in its argument types but do not appear in its result type (phantom). Matthieu Dubet GADT and Object-Oriented Programming
we want GADTs? GADTs in functional languages Generalizing datatypes Illustration of those rules in Haskell data Exp t where Lit :: Int -> Exp Int Plus :: Exp Int -> Exp Int -> Exp Int Equals :: Exp Int -> Exp Int -> Exp Int Cond :: Exp Bool -> Exp a -> Exp a -> Exp a Tuple :: Exp a -> Exp b -> Exp (a,b) Fst :: Exp (a,b) -> Exp a ... All constructors except for Cond make use of (1) All constructors except for Lit make use of (2) Fst uses a hidden type b, thus making use of (3) Matthieu Dubet GADT and Object-Oriented Programming
and GADTs Limits and extensions Plan 1 Introduction 2 Understanding GADTs 3 GADT in C# Object-Oriented features and GADTs Limits and extensions 4 Backup slides Matthieu Dubet GADT and Object-Oriented Programming
and GADTs Limits and extensions What can we do with current OOP languages? The first obversation of this paper is that the current type system of C# or Java is powerful enough to support a certain amount of GADTs manipulating programs. 1 Feature (1) is expressed by defining a subclass of a generic type that dit not just propagate the type parameters through the subclass. 2 Feature (2) corresponds to the existence of fields in the subclass whose types are arbitrary instantiations of the generic type of the superclass. 3 Feature (3) corresponds to the declaration of type parameters on the subclass that are not referenced in the superclass Matthieu Dubet GADT and Object-Oriented Programming
and GADTs Limits and extensions Typed evaluator in C# public abstract class Exp<T> { public abstract T Eval();} public class Lit : Exp<int> { ... public override int Eval() { return value ; } } public class Bool : Exp<bool> { ... public override bool Eval() { return value ; } } public class Plus : Exp<int> { Exp<int> e1,e2 ; public Plus ( Exp<int> e1, Exp<int> e2) { this.e1 = e1; this.e2 = e2 ; } public override int Eval() { return e1.Eval() + e2.Eval(); // No cast anymore } } Matthieu Dubet GADT and Object-Oriented Programming
and GADTs Limits and extensions Typed evaluator in C# static void Main () { Exp<int> error = new Plus ( new Lit(1), new Bool(true)) ; } typed.cs(30,23): error CS1502: The best overloaded method match for ‘Plus.Plus(Exp<int>, Exp<int>)’ has some invalid arguments typed.cs(30,23): error CS1503: Argument ‘#2’ cannot convert ‘Bool’ expression to type ‘Exp<int>’ That’s excellent because we have reached the same level of correctness as the OCaml implementation! Unfortunately, not everything goes that great in OOPL. Matthieu Dubet GADT and Object-Oriented Programming
and GADTs Limits and extensions Extending the evaluator public abstract class Exp<T> { public virtual bool Eq(Exp<T> that) {return false;} public virtual bool TupleEq<C,D>(Tuple<C,D> that) {return false;} public virtual bool LitEq(Lit that) {return false;} } public class Lit : Exp<int> { public override bool Eq (Exp<int> that) { return that.LitEq(this) ;} public override bool LitEq (Lit that) { return value == that.value;} } As usual with binary method such as Eq : Double dispatching, first on this, to the code that overrides Eq, then on that, to the code specific to the types of both this and that. Limitation: this is clumsy and non-modular. Matthieu Dubet GADT and Object-Oriented Programming
and GADTs Limits and extensions Extending the evaluator But there is worse, sometimes it’s just wrong: public class Tuple<A,B> : Exp<Pair<A,B>> { public override bool Eq (Exp<Pair<A,B>> that) { return that.TupleEq<A,B>(this) ;} public override bool TupleEq<C,D> (Tuple<C,D> that) { Tuple<A,B> That = (Tuple<A,B>) (object) that; return That.e1.Eq(e1) && That.e2.Eq(e2); } } Matthieu Dubet GADT and Object-Oriented Programming
and GADTs Limits and extensions Extending the evaluator But there is worse, sometimes it’s just wrong: public class Tuple<A,B> : Exp<Pair<A,B>> { public override bool Eq (Exp<Pair<A,B>> that) { return that.TupleEq<A,B>(this) ;} public override bool TupleEq<C,D> (Tuple<C,D> that) { Tuple<A,B> That = (Tuple<A,B>) (object) that; return That.e1.Eq(e1) && That.e2.Eq(e2); } } An ugly double cast from Tuple<C,D> to object (!) to Tuple<A,B> (which is also the type of this). Matthieu Dubet GADT and Object-Oriented Programming
and GADTs Limits and extensions Explaining the problem The fundamental problem is that the specialized code for TupleEq cannot make use of the fact that both that and this are of the same type. More generally: Information about a type instantiation is propagated through subclass refinement But there is no way to constrain the type of the receiver. Matthieu Dubet GADT and Object-Oriented Programming
and GADTs Limits and extensions Equational constraints in C# To resolve this problem, the paper propose a ”modest” extension of the existing type constraint (using keyword) mechanism. Current C# constraints are upper bounds wiht respect to subtyping. This proposal is to allow equational constraints: the requirement that two types be equal for a generic instantiation to be valid. Matthieu Dubet GADT and Object-Oriented Programming
and GADTs Limits and extensions A slight modification to the inheritance mechanism Like subtype constrains, equational constraints are inherited and do not need to be redeclared. But it’s now possible for some equations to be unsatisfiable in their inherited form at the override. For example, Lit class and the inherited abstract TupleEq method): int=Pair<C,D> . Proposed solution: relax the C# rule which mandates implementations for all interface methods and abstract virtual in (non-abstract) subclasses, but only when inherited constraints would be unsatisfiable. Matthieu Dubet GADT and Object-Oriented Programming
and GADTs Limits and extensions Question 3: How to improve our previous evaluator with an equational constraint Matthieu Dubet GADT and Object-Oriented Programming
and GADTs Limits and extensions Question 3: How to improve our previous evaluator with an equational constraint public abstract class Exp<T> { .. public virtual bool TupleEq<C,D> (Tuple<C,D> that) where T=Pair<C,D> { return false;} } public class Tuple<A,B> : Exp<Pair<A,B>> {... public override bool Eq (Exp<Pair<A,B>> that) { return that.TupleEq<A,B> (this) ;} public override bool TupleEq<C,D> (Tuple<C,D> that) { return that.e1.Eq(e1) && that.e1.Eq(e2); } } Matthieu Dubet GADT and Object-Oriented Programming
and GADTs Limits and extensions Question 3: How to improve our previous evaluator with an equational constraint public abstract class Exp<T> { .. public virtual bool TupleEq<C,D> (Tuple<C,D> that) where T=Pair<C,D> { return false;} } public class Tuple<A,B> : Exp<Pair<A,B>> {... public override bool Eq (Exp<Pair<A,B>> that) { return that.TupleEq<A,B> (this) ;} public override bool TupleEq<C,D> (Tuple<C,D> that) { return that.e1.Eq(e1) && that.e1.Eq(e2); } } No more casts! Matthieu Dubet GADT and Object-Oriented Programming
example C# Idiosyncrasies Plan 1 Introduction 2 Understanding GADTs 3 GADT in C# 4 Backup slides Simple temperature example C# Idiosyncrasies Matthieu Dubet GADT and Object-Oriented Programming
example C# Idiosyncrasies Implementation without GADT Let’s teach the compiler that ”You don’t mix apple with orange” type temp = | Kelvin of int | Celsius of int | Fahrenheit of int let add = function Kelvin t1, Kelvin t2 -> Kelvin(t1 + t2) | Celsius t1, Celsius t2 -> Celsius(t1 + t2) | Fahrenheit t1, Fahrenheit t2 -> Fahrenheit(t1+t2) | _,_ -> (failwith "Adding different units of temperature" ) let a = (Kelvin 1) and b = (Fahrenheit 5) in (* imagine a lot of code here ... *) (add a b) Unfortunately, the program computes correctly until the (add a b) which output the error. It would be better to catch the error at compile-time. Matthieu Dubet GADT and Object-Oriented Programming
example C# Idiosyncrasies Implementation with GADTs type kelvin type celsius type fahrenheit type _ temp = | K : int -> kelvin temp | C : int -> celsius temp | F : int -> fahrenheit temp # add (K 5) (F 4) ;; Error: This expression has type fahrenheit temp but an expression was expected of type kelvin te Thanks to GADTs, we enforce type correctness and the code doesn’t compile anymore. Matthieu Dubet GADT and Object-Oriented Programming
example C# Idiosyncrasies Question 4: Understanding C# Does it : Compile ? Crash ? Print ? int x = 9 + null ; System.Console.WriteLine(x); Matthieu Dubet GADT and Object-Oriented Programming