Introduction to ML Last time: This time, we continue writing an evaluator for a simple language Basics: integers, Booleans, tuples,... simple functions introduction to data types Pierce, Chapter 4 (in O’Caml) Show how to use: more functions exceptions modules A little language (LL) An arithmetic expression e is a boolean value an if statement (if e1 then e2 else e3) the number zero the successor of a number the predecessor of a number a test for zero (isZero e) LL abstract syntax in ML datatype term = Bool of bool | If of term * term * term | Zero | Successor of term | Predecessor of term | IsZero of term datatype declarations give you three things: 1. a new type 2. constructors functions for building objects of the new type 3. patterns for decomposing datatypes LL abstract syntax in ML If (Bool true, Zero, Successor (Successor Zero)) represents “if true then 0 else succ(succ 0)” If Booltrue Suc. Zero Suc. Zero Function declarations function name function parameter fun isNumberValue t = case t of Zero => true | Successor t2 => isNumberValue t2 | _ => false default pattern matches anything A very subtle error fun isNumberValue t = case t of zero => true | Successor t2 => isNumberValue t2 | _ => false The code above type checks. But when we test it refined the function always returns “true.” What has gone wrong? A very subtle error fun isNumberValue t = case t of zero => true | Successor t2 => isNumberValue t2 | _ => false The code above type checks. But when we test it refined the function always returns “true.” What has gone wrong? -- zero is not capitalized -- ML treats it like a variable pattern (matches anything!) Another function fun isNumberValue t = ... fun isValue t = case t of Bool _ => true | t => isNumberValue t Exceptions exception Error of string fun debug s : unit = raise (Error s) Exceptions exception Error of string fun debug s : unit = raise (Error s) in SML interpreter: - debug "hello"; uncaught exception Error raised at: ex.sml:15.28-15.35 Evaluator fun isNumberValue t = ... fun isValue t = ... exception NoRule fun eval1 t = case t of Bool _ | Zero => raise NoRule ... Evaluator ... fun eval1 t = case t of Bool _ | Zero => raise NoRule | If(Bool b,t2,t3) => (if b then t2 else t3) | If(t1,t2,t3) => If (eval1 t1,t2,t3) ... Evaluator exception NoRule fun eval1 t = case t of ... | Successor t => if isValue t then raise NoRule else let val t’ = eval1 t in Successor t’ end let expression: let declarations in expression end Finishing the Evaluator fun eval1 t = case t of ... | ... | Successor t => ... | Predecessor t => ... | IsZero t => ... be sure your case is exhaustive Finishing the Evaluator fun eval1 t = case t of ... | ... | Successor t => ... What if we forgot a case? Finishing the Evaluator fun eval1 t = case t of ... | ... | Successor t => ... What if we forgot a case? ex.sml:25.2-35.12 Warning: match nonexhaustive (Bool _ | Zero) => ... If (Bool b,t2,t3) => ... If (t1,t2,t3) => ... Successor t => ... Multi-step evaluation fun eval1 t = ... fun eval t = let fun loop t = loop (eval1 t) val message = “Done\n” in ((loop t) handle NoRule => print message | Error s => print s) end Be very careful with the syntax of handle (use extra parens) Done with the evaluator ML is all about functions There are many different ways to define functions! I almost always use “fun f x = ...” When I am only going to use a function once and it is not recursive, I write an anonymous function: (fn x => ...) Anonymous functions val n = 3 binds a variable (n) to a value (3) binds a variable (isValue) val isValue = (fn t => case t of Bool _ => true fn keyword | t => isNumberValue t) introduces anonymous fun to the anonymous function value Anonymous functions a type definition (very convenient) type ifun = int -> int val intCompose : ifun * ifun -> ifun = ... a pair of anonymous functions fun add3 x = intCompose ((fn x => x + 2), (fn y => y + 1)) x Anonymous functions type ifun = int -> int pattern match against arg val intCompose : ifun * ifun -> ifun = fn (f,g) => argument is pair of functions (fn x => f (g x)) result is a function! fun add3 x = intCompose ((fn x => x + 2), (fn y => y + 1)) x Another way to write a function fun f x = ........ can be written as: val f = (fn x => ......) provided the function is not recursive; f does not appear in ........ Another way to write a function fun f x = .... can always be written as: val rec f = (fn x => ...f can be used here...) keyword rec declares a recursive function value Yet another way to write a function fun isNumberValue Zero = true | isNumberValue (Successor t2) = true | isNumberValue (_) = true This is just an abbreviation for fun isNumberValue t = case t of Zero => true | Successor t2 => true | _ => true Yet another way to create a type error fun isNumberValue 0 = true | isNumberValue (Successor t2) = true | isNumberValue (_) = true ex.sml:9.1-11.29 Error: parameter or result constraints of clauses don't agree [literal] this clause: term -> 'Z previous clauses: int -> 'Z in declaration: isNumberValue = (fn 0 => true | Successor t2 => true | _ => true) Parametric Polymorphism Functions like compose work on objects of many different types val compose = fn f => fn g => fn x => f (g x) compose (fn x => x + 1) (fn x => x + 2) compose not (fn x => x < 17) Parametric Polymorphism Functions like compose work on objects of many different types val compose = fn f => fn g => fn x => f (g x) compose not (fn x => x < 17) compose (fn x => x < 17) not BAD!! Parametric Polymorphism Functions like compose work on objects of many different types val compose = fn f => fn g => fn x => f (g x) a type variable stands for any type compose: (‘a -> ‘b) -> (‘c -> ‘a) -> (‘c -> ‘b) Note: type variables are written with ‘ Parametric Polymorphism Functions like compose work on objects of many different types compose: (‘a -> ‘b) -> (‘c -> ‘a) -> (‘c -> ‘b) can be used as if it had the type: compose: (int -> ‘b) -> (‘c -> int) -> (‘c -> ‘b) Parametric Polymorphism Functions like compose work on objects of many different types compose: (‘a -> ‘b) -> (‘c -> ‘a) -> (‘c -> ‘b) can be used as if it had the type: compose: ((int * int) -> ‘b) -> (‘c -> (int * int)) -> (‘c -> ‘b) Parametric Polymorphism Functions like compose work on objects of many different types compose: (‘a -> ‘b) -> (‘c -> ‘a) -> (‘c -> ‘b) can be used as if it had the type: compose: (unit -> int) -> (int -> unit) -> (int -> int) Parametric Polymorphism compose : (‘a -> ‘b) -> (‘c -> ‘a) -> (‘c -> ‘b) not : bool -> bool compose not : ?? Parametric Polymorphism compose : (‘a -> ‘b) -> (‘c -> ‘a) -> (‘c -> ‘b) not : bool -> bool compose not : ?? type of compose’s argument must equal the type of not: bool -> bool == (‘a -> ‘b) Parametric Polymorphism compose : (‘a -> ‘b) -> (‘c -> ‘a) -> (‘c -> ‘b) not : bool -> bool compose not : ?? therefore: ‘a must be bool ‘b must be bool as well (in this use of compose) Parametric Polymorphism compose : (‘a -> ‘b) -> (‘c -> ‘a) -> (‘c -> ‘b) not : bool -> bool ‘a = bool ‘b = bool compose not : (‘c -> bool) -> (c’ -> bool) Parametric Polymorphism compose : (‘a -> ‘b) -> (‘c -> ‘a) -> (‘c -> ‘b) not : bool -> bool compose not : (‘c -> bool) -> (c’ -> bool) (compose not) not : bool -> bool Parametric Polymorphism compose : (‘a -> ‘b) -> (‘c -> ‘a) -> (‘c -> ‘b) not : bool -> bool compose not : (‘c -> bool) -> (c’ -> bool) (compose not) not : ?? Parametric Polymorphism compose : (‘a -> ‘b) -> (‘c -> ‘a) -> (‘c -> ‘b) compose (fn x => x) : ? Parametric Polymorphism compose : (‘a -> ‘b) -> (‘c -> ‘a) -> (‘c -> ‘b) compose (fn x => x) : ? ‘d -> ‘d Parametric Polymorphism compose : (‘a -> ‘b) -> (‘c -> ‘a) -> (‘c -> ‘b) compose (fn x => x) : ? ‘d -> ‘d must be the same ie: ‘a = ‘d ‘b = ‘d Parametric Polymorphism compose : (‘d -> ‘d) -> (‘c -> ‘d) -> (‘c -> ‘d) compose (fn x => x) : ? ‘d -> ‘d must be the same ie: ‘a = ‘d ‘b = ‘d Parametric Polymorphism compose : (‘d -> ‘d) -> (‘c -> ‘d) -> (‘c -> ‘d) compose (fn x => x) : ? ‘d -> ‘d (‘c -> ‘d) -> (‘c -> ‘d) Lists Lists: nil : ‘a list :: : ‘a * ‘a list -> ‘a list 3 :: 4 :: 5 :: nil : int list (fn x => x) :: nil : (‘a -> ‘a) list Lists Lists: [] : ‘a list a different way of writing “nil” 3 :: [4, 5] : int list [fn x => x] : (‘a -> ‘a) list List Processing Functions over lists are usually defined by case analysis (induction) over the structure of a list fun length l = case l of nil => 0 l _ :: l => 1 + (length l) Hint: often, the structure of a function is guided by the type of the argument (recall eval) List Processing fun map f l = case l of nil => [] l x :: l => (f x) :: (map f l) an incredibly useful function: - map (fn x => x+1) [1,2,3]; > val it = [2,3,4] ; int list List Processing fun fold f a l = case l of nil => a l x :: l => f (fold f a l) x another incredibly useful function what does it do? what is its type? use it to write map. ML Modules Signatures Structures Interfaces Implementations Functors Parameterized structures Functions from structures to structures Structures structure Queue = struct type ‘a queue = ‘a list * ‘a list exception Empty val empty = (nil, nil) fun insert (x, q) = … fun remove q = … end Structures structure Queue = struct type ‘a queue = ‘a list * ‘a list exception Empty ... end fun insert2 q x y = Queue.insert (y, Queue.insert (q, x)) Structures structure Queue = struct ... end structure Q = Queue fun insert2 q x y = Q.insert (y, Q.insert (q, x)) convenient abbreviation Structures structure Queue = struct ... end for lazy programmers -- not encouraged! open Queue fun insert2 q x y = insert (y, insert (q, x)) Structures structure Queue = struct type ‘a queue = ‘a list * ‘a list ... end fun insert2 (q1,q2) x y : ‘a queue = (x::y::q1,q2) by default, all components of the structure may be used -- we know the type ‘a queue Signatures abstract type signature QUEUE = -- we don’t know the sig type ‘a queue type ‘a queue exception Empty val empty : ‘a queue val insert : ‘a * ‘a queue -> ‘a queue val remove : ‘a queue -> ‘a * ‘a queue end Information hiding signature QUEUE = sig type ‘a queue ... end structure Queue :> QUEUE = struct type ‘a queue = ‘a list * ‘a list val empty = (nil, nil) … end does not type check fun insert2 (q1,q2) x y : ‘a queue = (x::y::q1,q2) Signature Ascription Opaque ascription Provides abstract types structure Queue :> QUEUE = … Transparent ascription A special case of opaque ascription Hides fields but does not make types abstract structure Queue_E : QUEUE = … SEE Harper, chapters 18-22 for more on modules Other things functors (functions from structures to structures) references (mutable data structures) ref e; !e; e1 := e2 while loops, for loops arrays (* comments (* can be *) nested *) a bunch of other stuff... Last Things Learning to program in SML can be tricky at first But once you get used to it, you will never want to go back to imperative languages Check out the reference materials listed on the course homepage