Polymorphism ------------ Means "many shapes". Want to reuse code in different contexts. Problem boils down to we want a variable to be able to store values with different types. In a dynamically typed language, a variable can have a value of any type. Before performing an operation, check the type to see if the operation is legal. In a statically typed language, a variable has a fixed type. Want types expressive enough to allow polymorphism, but still type safe. Various ways to do this: - Subtype polymorphism - Parametric polymorphism - Also: introduce a 'dynamic' type and explicit type-checks Subtyping --------- In subtype polymorphism, a variable can store any subtype of the declared type. What's a subtype? T1 is a subtype of T2 if a value of T1 can be used anywhere a T2 can be. Write T1 <: T2. Because of substitutability, a subtype must support all the operations of its supertype. It can support more operations. OO languages support subtyping. A subclass is (usually) a subtype. A subclass has the same interface as the supertype plus maybe some more methods. Anywhere I have a variable of type Animal, I can store a subtype (subclass) of Animal. Say Dinosaur. Anywhere I call a method or access a field of an Animal, I can do the same for a value of type Dinosaur. Non OO languages can also have subtyping. We'll talk more about subtyping next week. Parametric polymorphism ------------------------ Another type of polymorphism is parametric polymorphism. The idea here is to introduce _type variables_. A type variable 'a' can be _instantiated_ on multiple types. Generics in Java, C#, Scala, etc are a form of parametric polymorphism. These languages are OO and support both subtype polymorphism and parametric polymorphism. The interaction between these features is quite complicated and we'll talk about this next week. In Haskell, we can have type variables in datatype declarations: data List a = Nil | Cons a (List a) Or in function types: mylength :: List a -> Int mylength Nil = 0 mylength (Cons hd tl) = 1 + mylength tl mymap :: (a -> b) -> List a -> List b mymap f Nil = Nil mymap f (Cons hd tl) = Cons (f hd) (mymap f tl) More formally, the type of mylength is: forall a . List a -> Int The type of mymap is: forall a b . (a -> b) -> List a -> List b Polymorphic lambda calculus --------------------------- We can formalize this with the _polymorphic lambda calculus_. Or System F. Due to John Reynolds 1974. e ::= n | x | e1 e2 | \x:T.e | /\X.e | e[T] T ::= Int | T -> T | X | forall X.T The /\ expression introduces a type variable into scope. The e[T] expression instantiates a type variable with a concrete type. Typing rules: We extend the context with the set D of type variables in scope. D; G |- n : Int D; G |- x : G(x) D; G |- e1 : T1 -> T2 D; G |- e2 : T1 ---------------- D; G |- e1 e2 : T2 D; G, x:T1 |- e : T2 D |- T1 <---- T1 is well-formed --------------------- D; G |- \x:T1.e : T1 -> T2 D, X; G |- e : T ----------------------- D; G |- /\X.e : forall X. T D; G |- e : forall X. T1 D |- T <---- T is well-formed ------------------- D; G |- e[T] : T1[X:=T] Example: identity function X; x:X |- x : X -------------------------- X; . |- \x: X . x : X -> X ------------------------------------------ .; . |- /\X . \x: X . x : forall X. X -> X Example: using the identity function .; . |- id : forall X. X -> X ----------------------------- .; . |- id[Int] : Int -> Int .; . |- n : Int -------------------------------------------------- .; . |- id[Int] n : Int Example: doubling ---------------------------------------------------------------- /\X. \f: X -> X. \a: X . f (f a) : forall X . (X -> X) -> X -> X double[Nat] : (Nat -> Nat) -> Nat -> Nat double[Nat -> Nat] : ((Nat -> Nat) -> (Nat -> Nat)) -> (Nat -> Nat) -> (Nat -> Nat) Example: omega (homework) \x . x x cannot be typed in STLC. But we can type a similar term in System F. \x : forall X. X -> X . x[forall X. X -> X] x Parametricity ------------- Let's examine a feature of this language. Suppose we have a value of type: f : forall X. X -> X What are the possible values of f? Put another way if I pass a value v into f, what possible results do I get? f v or more precisely: f[T] v where v is of type T. Answer: v MUST be the identity function. Why? f's formal parameter is of type X. f must accept a value of any type. There is no way for f to examine the value. It has no side effects; it cannot compare v to another value. It doesn't know anything about v, so it can't even compare it to another value v. Even if it did, it can't manufacture another value of the same type because it doesn't know what type the argument will be. The only possible values of f are: f = id = \x . x and f = \x . _|_ (a function that diverges) We could try to write f differently: f = \x . (\x . x) x But this behaves the same as id. We could try: f = \x . 0 But 0 is the wrong type. It's not an X, it's an Int. This function has type forall X. X -> Int. This principle is called _parametricity_. The types constrain the possible values of the given type. Because the value must be usable with any possible instantiation of the types. This only works with type variables. If we have a type: Int -> Int There are many values of this type. id x = x double x = 2*x triple x = 3*x abs x = if x >= 0 then x else (-x) zero x = 0 If we have a type: a -> a There is only one possible value: the identity function (ignoring nontermination). How about this type: a -> a -> a There are two possible values of this type: \x . \y . x \x . \y . y How about: (a, b) -> (b, a) This MUST swap the pair. More: uncurry : (a -> b -> c) -> (a, b) -> c uncurry f (x, y) = f x y dup : a -> (a, a) dup x = (x, x) fsta : (a, a) -> a fsta (x, y) = x snda : (a, a) -> a snda (x, y) = y How about: [a] -> [a] This could be: id tail reverse \xs -> xs ++ xs BUT, it can't be: sort It can't actually examine the values of the input list, since they're abstract. Therefore, a function of this type CANNOT be 'sort'. You need to pass in a comparison function. sort : forall a. (a -> a -> Bool) -> [a] -> [a] Then call as: sort[Int] (<) [3,2,1] --> [1,2,3] Parametricity is a powerful and useful result. Functional (Haskell) programming is often just about getting the types right. If the types are right, it will often just work because there's no other choice. Type classes ------------ Parametricity is powerful, but it means you often have to pass in functions to, say, compare values. This gets tedious. Haskell provides 'type classes' to avoid this problem. Let's look at 'sort'. We'd like to be able to use sort like the following: > import Data.List > sort [3,4,2,1] [1,2,3,4] And in Haskell, indeed we can! But we just said we can't write a function: sort :: [a] -> [a] So maybe sort works for Ints only: sort :: [Int] -> [Int] But no: > sort ["b", "a", "c"] ["a", "b", "c"] It works with Strings also. The key is in the type of sort: > :t sort sort :: Ord a => [a] -> [a] sort is of type [a] -> [a] but it has a _predicate_ on the types 'Ord'. sort can be called with any list of 'a' where 'Ord a' holds. 'Ord' is called a type class. Let's look at the definition of 'Ord': > :browse Data.Ord class Eq a => Ord a where compare :: a -> a -> Ordering (<) :: a -> a -> Bool (>=) :: a -> a -> Bool (>) :: a -> a -> Bool (<=) :: a -> a -> Bool max :: a -> a -> a min :: a -> a -> a data Ordering = LT | EQ | GT > :browse Data.Eq class Eq a where (==) :: a -> a -> Bool (/=) :: a -> a -> Bool This says that 'a' satisfies 'Ord a' if there are functions compare, <, >=, etc. In addition, an Ord must also be an Eq, which defines == and /=. Type classes are used everywhere in Haskell. You've already seen them: > :t show show :: Show a => a -> String > :t 1 1 :: Num a => a Type classes let us get around parametricity by letting us write functions that have a particular set of operations. This does not actually break parametricity because what, say, Eq a => [a] -> [a] is really doing is desugaring into something like this: data Eq a = Eq { (==) :: a -> a -> Bool (/=) :: a -> a -> Bool } Eq a -> [a] -> [a] Every function that is guarded by Eq is passed a _dictionary_ as an implicit argument. This is starting to look very OO. You create the dictionary by instantiationg the class: instance Eq String where "" == "" = true (s1:ss1) == (s2:ss2) = s1 == s2 && ss1 == ss2 _ == _ = False ...