Lecture 10: Types, Type checking, & Type inference

CSC 131 Spring, 2019

Kim Bruce

Programming Language Rankings Top Combined

1 JavaScript 11 Swift 2 12 Scala 3 Python 12 Shell 4 PHP 14 Go 5 # 14 R 6 C++ 16 TypeScript 7 CSS 17 PowerShell 8 Ruby 18 Perl 9 C 19 Haskell 9 Objective-C 20 Lua Tiobe Index Changes over Time

2014 -> 2019 Scala #41 -> 29 Haskell #44 -> 39 ML #25 -> 48 Go #38 -> 18 Dart #42 -> 25 Scheme #48 -> 36 Objective C # 3 -> 10 Swift # 18 -> 20

Static Type Checking Type checking • Most statically typed languages also include • Static type-checkers for strongly-typed some dynamic checks. languages (i.e., rule out all “bad” programs) - array bounds. must be conservative: - Java’s instanceof - Rule out some programs without errors. - typecase or type casts • if (program-that-could-run-forever) { expression-w-type-error; • Pascal statically typed, but not strongly typed } else { - variant records (essentialy union types), dangling pointers expression-w-type-error; } • Haskell, ML, Java strongly typed • C, C++ not strongly typed Type Compatibility Structural Equivalence

• When is x := y legal? Type T = Array [1..10] of Integer; Var A, B : Array [1..10] of Integer; • Can be subtle: C : Array [1..10] of Integer; T1 = record a : integer; b : real end; : T; T2 = record c : integer; d : real end; E : T; T3 = record b : real; a : integer end; • Which are the same? • Name EquivalenceA (Ada) T = record info : integer; next : ^T end; U = record info : integer; next : ^V end; • Name Equivalence (Pascal, Modula-2, Java) V = record info : integer; next : ^U end; • Structural Equivalence (Modula-3, Java arrays only)

Type Checking & Inference Formal Type-Checking Rules

• Can rewrite more formally. • Write explicit rules. Let a, b be expressions • Expression may involve variables, so type check - if a, b:: Integer, wrt assignment E of types to variables. then a+b, a*b, a div b, a mod b:: Integer - E.g., E(x) = Integer, E(b) = Bool, ... - if a, b:: Integer then a < b, a = b, a > b : Bool Hypothesis E(x) = t –––––––––––––– - if a, b: Bool then a && b, a || b: Bool E |- x : t Conclusion - ... E |- a : int, E |- b : int –––––––––––––––––––––––––––– E |- a+b : int Can write formally

Function Application: E |- f: σ → τ, E |- M : σ –––––––––––––––––––––– E |- f(M) : τ

Function Definition: Haskell Type Inference E ∪ {v:σ} |- Block : τ –––––––––––––––––––––––– How does Haskel know what you meant? E |- fun (v:σ) Block : σ → τ

Can write for al language constructs. Based on context fee grammar. Can read off type-checking algorithm. More later!

Haskell Type Inference Examples of Type Inference

1. An identifier should be assigned the same type Use rules to deduce types: throughout its scope. • 2. In an “if-then-else” expression, the condition must have map = \ f -> \ l -> type Bool and the “then” and “else” portions must have if l == [] then [] the same type. The type of the expression is the type of else (f (head l)): (map f (tail l)) the “then” and “else” portions. map:: a b because function 3. A user-defined function has type a → b, where a is the - → type of the function’s parameter and b is the type of its - f :: a, \ l -> ... :: b, Thus b = c → d result. - l :: c, if l = [] then ... :: d 4.In a function application of the form f x, there must be types a and b such that f has type a → b, x has type a, - ... and the application itself has type b. @ = application Outcome of Type Inference λ :a

( , ) @ :b • Overconstrained: no solution Prelude> tail 7 @ :c :1:5: No instance for (Num [a]) x arising from the literal `7' at :1:5 f :d :e Possible fix: add an instance declaration for (Num [a]) In the first argument of `tail', namely `7' In the expression: tail 7 In the definition of `it': it = tail 7 • Underconstrained: polymorphic double(f,x) = f(f(x)) Uniquely determined or equivalently • double = \ (f,x) -> f(f(x))

Restrictions on ML/Haskell By the way, ... Polymorphism

• Inference due to Hindley-Milner • Type (a → b) →([a] → [b]) stands for: • SML/Haskell type inference is doubly - ∀a. ∀b. (a → b) →([a] → [b]) exponential in the worst case! • Haskell functions may not take polymorphic arguments. E.g., no type: Can write down terms tn of length n such that • ∀b. ((∀a.(a → a)) →(b → b)) n - the length of the type of tn is of length 22 - define: foo f (x,y) = (f x, f y) • Luckily, it doesn’t matter in practice, - id z = z - no one writes terms whose type is exponential in the - foo id (7, True) -- gives type error! length of the term! - Type of foo is only (t -> s) -> (t, t) -> (s, s) Restrictions on Implicit Explicit Polymorphism Polymorphism

Polymorphic types can be defined at top level or in let clauses, but can’t be used as arguments of functions Easy to type w/ explicit polymorphism:

id x = x test (g: forall t.t -> t) = (g “ab”, g 17) in (id "ab", id 17) in test (\t => \(x:t) -> x) OK, but can't write

test g = (g “ab”, g 17) Languages w/explicit polymorphism: Clu, Ada, C++, Eiffel, Java 5, C#, Scala, Grace Can’t find type of test w/unification. More general type inference is undecidable.

Explicit Polymorphism Summary

• Modern tendency: strengthen typing & avoid • Clu, Ada, C++, Java implicit holes, but leave explicit escapes • C++ macro expanded at link time rather than • Push errors closer to by: compile time. - Require over-specification of types • Java compiles away polymorphism, but checks - Distinguishing between different uses of same type it statically. - Mandate constructs that eliminate type holes • Better implementations keep track of type - Minimizing or eliminating explicit pointers parameters. • Holy grail: Provide , increase flexibility Polymorphism vs Overloading Why Overloading? • • Many useful functions not parametric - Single algorithm may be given many types - List membership requires equality - Type variable may be replaced by any type • member: [w] -> w -> Bool (for “good” w) • Examples: hd, tail ::[t]->t , map::(a->b)->[a]->[b] - Sorting requires ordering • Overloading • sort: [w] -> [w] (for w supporting <,>,...) - A single symbol may refer to more than one algorithm. • What are problems in supporting it in a PL? - Each algorithm may have different type. - Static type inference makes it hard! - Choice of algorithm determined by type context. - Why are Haskell type classes a solution? • (+) has types Int → Int → Int and Float → Float → Float, but not t→t→t for arbitrary t.

Overloading Arithmetic ML & Overloading

• First try: allow fcns w/overloaded ops to define • Functions like +, * can be overloaded, but not multiple functions functions defined from them! 3 * 3 -- legal - square x = x * x • • 3.14 * 3.14 -- legal • versions for Int -> Int and Float -> Float • square x = x * x -- Int -> Int But then - • square 3 -- legal squares (x,y,z) = (square x, square y, square z) • • square 3.14 -- illegal • ... has 8 different versions!! - To get other functions, must include type: Too complex to support! - • squaref (x:float) = x * x -- float -> float Equality Type Classes

• Proposed for Haskell in 1989. • Equality worse! - Only defined for types not containing functions, files, • Provide concise types to describe overloaded or abstract types -- why? functions -- avoiding exponential blow-up - Again restrict functions using == • Allow users to define functions using overloaded • ML ended up defining eq types, with special operations: +, *, <, etc. mark on type variables. • Allow users to declare new overloaded functions. - member: ‘‘a -> [‘‘a] -> Bool - Generalize ML’s eqtypes - Can’t apply to list of functions - Fit within type inference framework

Recall ... Example Recall • Definition of quicksort & partition: • partition lThan (pivot, []) = ([],[]) class Order a where partition lThan (pivot, first : others) = (<) :: a -> a -> Bool let (>) :: a -> a -> Bool (smalls, bigs) = partition lThan (pivot, others) ... in if (lThan first pivot) then (first:smalls, bigs) • Implement w/dictionary: else (smalls, first:bigs) data OrdDict a = MkOrdDict (a -> a -> Bool) (a -> a -> • Allowed partition to be parametric Bool) ... - Steal this idea to pass overloaded functions! getLT (MkOrdDict lt gt ...) = lt - Implicitly pass argument with any overloaded getGT (MkOrdDict lt gt) = gt functions needed!! ... Using Dictionaries Instances

partition dict (pivot, []) = ([],[]) partition dict (pivot, first : others) = let (smalls, bigs) = partition dict (pivot, others) • Declaration in if ((getLT dict) first pivot) - instance Show TrafficLight where then (first:smalls, bigs) show Red = "Red light" else (smalls, first:bigs) show Yellow = "Yellow light" show Green = "Green light" partition:: OrdDict a -> [a] -> ([a].[a]) - Creates dictionary for “show” Compiler adds dictionary parameter to al cals of partition. Reports type of partition (w/out lThan parameter) as (Ord a) => [a] -> ([a].[a])

Implementation Summary Multiple Dictionaries

• Compiler translates each function using an overloaded symbol into function with extra parameter: the dictionary. • Example: • References to overloaded symbols are rewritten by the compiler to lookup the symbol in the dictionary. - squares :: (Num a, Num b, Num c) => (a, b, c) -> (a, b, c) • The compiler converts each declaration into a - squares(x,y,z) = (square x, square y, square z) dictionary type declaration and a set of selector functions. • goes to: The compiler converts each instance declaration into a • squares (da,db,dc) (x, y, z) = dictionary of the appropriate type. - (square da x, square db y, square dc z) • The compiler rewrites calls to overloaded functions to pass a dictionary. It uses the static, qualified type of the function to select the dictionary.