CSCC24 Functional Programming Typing, Scope, Exceptions ML Carolyn MacLeod 1 winter 2012 1 Based on slides by Anya Tafliovich, with many thanks to Gerald Penn and Sheila McIlraith.
motivation Consider the following Scheme function definition: (define foobar (lambda (x) (if (even? x) (first x) (rest x)))) Anything wrong?
motivation Consider the following Scheme function definition: (define foobar (lambda (x) (if (even? x) (first x) (rest x)))) Anything wrong? even? expects a number and first, rest expect a list = we ll get an error at run-time.
motivation Consider the following Scheme function definition: (define foobar (lambda (x) (if (even? x) (first x) (rest x)))) Anything wrong? even? expects a number and first, rest expect a list = we ll get an error at run-time. We are able to figure it out before run-time. Can t a PL support this kind of reasoning?
What is the property of Scheme that is related to this property
3 What is the property of Scheme that is related to this property Dynamic typing
3 What is the property of Scheme that is related to this property Dynamic typing What other language do you know with dynamic typing?
3 What is the property of Scheme that is related to this property Dynamic typing What other language do you know with dynamic typing? Python
It could be worse! motivation
It could be worse! motivation At least Scheme checks that x is a list (cons pair) before accessing memory in the execution of (rest x).
It could be worse! motivation At least Scheme checks that x is a list (cons pair) before accessing memory in the execution of (rest x). In fact, Scheme is type safe: it will never execute if op is not applicable to arg. (op arg)
It could be worse! motivation At least Scheme checks that x is a list (cons pair) before accessing memory in the execution of (rest x). In fact, Scheme is type safe: it will never execute if op is not applicable to arg. (op arg) What s a language you know that is not type safe?
It could be worse! motivation At least Scheme checks that x is a list (cons pair) before accessing memory in the execution of (rest x). In fact, Scheme is type safe: it will never execute if op is not applicable to arg. (op arg) What s a language you know that is not type safe? C is one you most likely know.
It could be worse! motivation At least Scheme checks that x is a list (cons pair) before accessing memory in the execution of (rest x). In fact, Scheme is type safe: it will never execute if op is not applicable to arg. (op arg) What s a language you know that is not type safe? C is one you most likely know. Languages which are not type safe (e.g., C) allow unsafe memory accesses: a major source of security vulnerabilities.
5 motivation Want to catch all errors at compile-time.
5 motivation Want to catch all errors at compile-time. Extremely difficult, if not impossible.
5 motivation Want to catch all errors at compile-time. Extremely difficult, if not impossible. Identify a certain class of errors and guarantee to catch all of them.
5 motivation Want to catch all errors at compile-time. Extremely difficult, if not impossible. Identify a certain class of errors and guarantee to catch all of them. Catch as many errors as possible?
5 motivation Want to catch all errors at compile-time. Extremely difficult, if not impossible. Identify a certain class of errors and guarantee to catch all of them. Catch as many errors as possible? Costly: programmers expertise, computational resources, etc.
5 motivation Want to catch all errors at compile-time. Extremely difficult, if not impossible. Identify a certain class of errors and guarantee to catch all of them. Catch as many errors as possible? Costly: programmers expertise, computational resources, etc. Slower development, less expressive languages.
5 motivation Want to catch all errors at compile-time. Extremely difficult, if not impossible. Identify a certain class of errors and guarantee to catch all of them. Catch as many errors as possible? Costly: programmers expertise, computational resources, etc. Slower development, less expressive languages. Used in development of safety-critical systems.
5 motivation Want to catch all errors at compile-time. Extremely difficult, if not impossible. Identify a certain class of errors and guarantee to catch all of them. Catch as many errors as possible? Costly: programmers expertise, computational resources, etc. Slower development, less expressive languages. Used in development of safety-critical systems. Compromise: type systems.
5 motivation Want to catch all errors at compile-time. Extremely difficult, if not impossible. Identify a certain class of errors and guarantee to catch all of them. Catch as many errors as possible? Costly: programmers expertise, computational resources, etc. Slower development, less expressive languages. Used in development of safety-critical systems. Compromise: type systems. Guarantees range widely: from basic safety of memory accesses to just about anything you want.
A type is: typing A name for a set of values and some operations which can be performed on that set of values. A collection of computational entities that share some common property.
7 typing Some examples of types (in ML notation): int : the integers (and their operations). real : the real numbers (and their operations). string : the strings (and their operations). bool : the booleans (and their operations). int bool : the functions that take integers as input and return booleans as output.
typing Some examples of types (in ML notation): int : the integers (and their operations). real : the real numbers (and their operations). string : the strings (and their operations). bool : the booleans (and their operations). int bool : the functions that take integers as input and return booleans as output. What constitutes a type is language dependent.
Benefits of having a type system: typing Easier to debug programs: compiler can catch many errors. Static analysis: a lot of useful information about the program can be obtained at compile-time. Efficiency: typing can be used by the compiler to generate quicker code. Correctness: typing can be used (by the programmer or by the compiler) to prove correctness of code. Documentation: types declare your intent with well-chosen names.
Recall - last lecture We saw this Scheme function definition: (define foobar (lambda (x) (if (even? x) (first x) (rest x)))) What is wrong with the code?
Recall - last lecture We saw this Scheme function definition: (define foobar (lambda (x) (if (even? x) (first x) (rest x)))) What is wrong with the code? What programming language feature would stop us creating programs with this kind of bugs?
10 typing A programming language is type safe if no program is allowed to violate its type distinctions. The process of verifying and enforcing the constraints of types is called type checking. Type checking can either occur at compile-time (static type checking) or at run-time (dynamic type checking).
11 Dynamic type checking: static vs dynamic typing Static type checking:
static vs dynamic typing Dynamic type checking: Performed at run-time. Slower execution: need to carry type information around, lots of run-time checks. More flexible. Easier refactoring. Static type checking: Faster execution. Compiler can do a lot of optimization. Some argue that resulting programs are safer. Some argue that resulting programs are more elegant and modular. Some argue that programmers will write horrible code to get around a static type-checker. 11
12 static typing Explicit static typing: code contains type annotations. For example, in Java: Variable declarations int x,y,z; Function headers public static void main(string[] arg)
static typing Explicit static typing: code contains type annotations. For example, in Java: Variable declarations int x,y,z; Function headers public static void main(string[] arg) Type inference: infer all types from the code that does not contain explicit type annotations. fun foo(x,y,z) = if x then y + z + 1.5 else 0.0;
static typing Explicit static typing: code contains type annotations. For example, in Java: Variable declarations int x,y,z; Function headers public static void main(string[] arg) Type inference: infer all types from the code that does not contain explicit type annotations. fun foo(x,y,z) = if x then y + z + 1.5 else 0.0; x must be boolean
static typing Explicit static typing: code contains type annotations. For example, in Java: Variable declarations int x,y,z; Function headers public static void main(string[] arg) Type inference: infer all types from the code that does not contain explicit type annotations. fun foo(x,y,z) = if x then y + z + 1.5 else 0.0; x y, z must be boolean must be reals
static typing Explicit static typing: code contains type annotations. For example, in Java: Variable declarations int x,y,z; Function headers public static void main(string[] arg) Type inference: infer all types from the code that does not contain explicit type annotations. fun foo(x,y,z) = if x then y + z + 1.5 else 0.0; x y, z must be boolean must be reals the return value is real
static typing Explicit static typing: code contains type annotations. For example, in Java: Variable declarations int x,y,z; Function headers public static void main(string[] arg) Type inference: infer all types from the code that does not contain explicit type annotations. fun foo(x,y,z) = if x then y + z + 1.5 else 0.0; x y, z must be boolean must be reals the return value is real foo : bool * real * real -> real
Features of Standard ML What is ML? ML is an ALGOL family language, a child of Pascal and Lisp, and a distant cousin of C ML was designed as the Meta- Language of the Logic for Computable Functions (LCF) System. Its original purpose was for writing programs that would attempt to construct mathematical proofs. Question - What have we seen that is similar? ML is a functional language, although ML can be written in a more imperative style. Arguably, you can call it a mostly functional language, or a function-oriented imperative language.
14 Features of Standard ML Modular:Supports modules and interfaces, determining what components and types from the module are visible outside. Strict Pass By Value: The arguments of the function are evaluated before the beginning of the function being evaluated. Polymorphic: Supports both data type and function polymorphism. Type System: The ML type system is considered one of the cleanest in any language Static typing Type inference Type safe Functions are first class values: Functions can be defined, passed as arguments, and returned as function results. Garbage Collection Exception Handling
A few examples Standard ML
ML Examples (* This is a comment in SML *) fun factorial (n:int):int = if n = 0 then 1 else n * factorial(n-1) Notice, we use = both for comparison, and for the function definition. We use an infix notation, unlike in Scheme. In this example, we have explicit static typing.
ML Examples Another form of writing function definitions: (* Our first fibonacci solution in Scheme, translated to ML *) fun fib 1 = 1 fib 2 = 1 fib (n) = fib(n-1) + fib(n-2) This is also an example of type inference - notice that this version does not specify types for the input and output of fib, however, when compiled or entered into the interpreter, it gives a type signature of val fib = fn : int -> int
18 type inference Trade-off: Want the language to be expressive. Want tractable type inference. ML is designed to make type inference tractable. Widely regarded as an important language innovation.
19 ML data types Basic types: unit : the only member is (). bool : booleans. int : integers. real : reals. string : strings.
19 ML data types Basic types: unit : the only member is (). bool : booleans. int : integers. real : reals. string : strings. More types: ( type 0 type 1... type n ) : tuples. type list : lists. input-type output-type : functions.
20 ML basic types unit: this type has only one element () - (); val it = () : unit ML assigns the last evaluated value to the special variable it. Reading the above snippet of the ML interpreter session: evaluate (), please the special variable it now has the value () of type unit
ML basic types bool: this type has two elements: true and false. Operations on bools: not, tostring,... Special operators: andalso, orelse. For example: - if (not true andalso false) orelse true then true else false; val it true : bool
ML basic types The structure of lists in ML is the same as in Scheme and Prolog. Strings must have elements of all the same type. - hd [1,2,3]; val it = 1 : int - tl [1,2,3]; val it = [2,3] : int list - "a" :: ["b","c"]; val it = ["a","b","c"] : string list - [1,2]@[3]; val it = [1,2,3] : int list These are the equivalents of first/car, rest/cdr, and cons in Scheme. The @ operator is used like append in Scheme.
ML basic types fun elem_to_list [] = [] elem_to_list (h::t) = [h] :: (elem_to_list t)
24 int: {... 2, 1, 0, 1, 2,... } ML basic types Operations: +,,,, div, mod, <, >, =, <=, >=, <> For example: - 5 + 6 * 2-3 div 2; val it = 16 : int - 5 mod 2 >= 6 mod 2; val it = true : bool
ML basic types int: {... 2, 1, 0, 1, 2,... } Note the use of ~ for negation. Here s why: - is a binary operator while ~ is a unary operator. - -5; stdin:27.1 Error: expression or pattern begins with infix identifier "-" stdin:27.1-27.3 Error: operator and operand don t agree [literal] operator domain: Z * Z operand: int in expression: - 5 - ~5; val it = ~5 : int
real: { 1.0, 3.14159, 11.7,... } ML basic types Operations: +,,, <, >, <=, >= Note: You cannot mix reals and integers in one expression. Every ML expression has a type. If an expression cannot be typed, we get an error.
For example: ML basic types - 2-1.5; stdin:43.1-43.8 Error: operator and operand don t agree operator domain: int * int operand: int * real in expression: 2-1.5 Understanding the error message... Why int? For mathematical operators, if the type is not specified (or inferred), then type int is assumed.
More examples: ML basic types - 2; val it = 2 : int - 2.0; val it = 2.0 : real - real(2); val it = 2.0 : real - 2.0-1.5; val it = 0.5 : real - real(2) - 1.5; val it = 0.5 : real
ML basic types Note that SML inspects the leftmost argument first. For example: - 1.5 + 2; stdin:1.1-2.3 Error: operator and operand don t agree operator domain: real * real operand: real * int in expression: 1.5 + 2 Now it expects two reals.
30 ML basic types Note that while <, <=, >, >= are defined for all numeric types, = and <> are, for example, not defined for reals. In fact, = : a * a -> bool <> : a * a -> bool In ML a means the type for which equality is defined. With reals we can use a <= b andalso b >= a. Or, much better, use real arithmetic: - Real.==(1.0, 1.0); val it = true : bool - Real.==(1.0, 1.00000); val it = true : bool
31 ML basic types string: { Hello, world, CSCC24 is fun!,... } Operations: ^ for concatenation For example: - "Hello"; val it = "Hello" : string - "Hello" ^ " " ^ "Jim"; val it = "Hello Jim" : string
32 ML data types Basic types: unit : the only member is (). bool : booleans. int : integers. real : reals. string : strings.
32 ML data types Basic types: unit : the only member is (). bool : booleans. int : integers. real : reals. string : strings. More types: ( type 0 type 1... type n ) : tuples. type list : lists. input-type output-type : functions.
ML types A tuple packs together several types. - ("foo", "bar"); val it = ("foo","bar") : string * string - ("foo", "bar", 123); val it = ("foo","bar",123) : string * string * int - ("foo", (123, 456)); val it = ("foo",(123,456)) : string * (int * int) Operations: accessing component N of a tuple t is written #N t. - #2 ("foo", 123, 3.14); val it = 123 : int
ML types In ML all elements in a list must have the same type. - [1,2,3,4]; val it = [1,2,3,4] : int list - ["cscc24","is","fun"]; val it = ["cscc24","is","fun"] : string list - [("foo",1.0),("bar",3.14)]; val it = [("foo",1.0),("bar",3.14)] : (string * real) list [ ] (or nil) is the empty list. Constructor: :: Selectors: hd, tl More operations: @, null, length, map, foldr,... 34
Some examples: ML types - "foo"::["bar","foobar"]; val it = ["foo","bar","foobar"] : string list - [1,2]@[3,4]; val it = [1,2,3,4] : int list - hd [1,2]; val it = 1 : int - tl [1,2]; val it = [2] : int list
36 ML syntax A variable declaration in ML looks like: val name = expr
36 ML syntax A variable declaration in ML looks like: val name = expr val x = 42 + 24
36 ML syntax A variable declaration in ML looks like: val name = expr val x = 42 + 24 (define x (+ 42 24))
37 ML functions The syntax for anonymous functions in ML is: fn arg => body
37 ML functions The syntax for anonymous functions in ML is: fn arg => body fn x => x + 1
37 ML functions The syntax for anonymous functions in ML is: fn arg => body fn x => x + 1 (lambda (x) (+ x 1))
37 ML functions The syntax for anonymous functions in ML is: fn arg => body fn x => x + 1 (lambda (x) (+ x 1))
ML functions The syntax for anonymous functions in ML is: fn arg => body fn x => x + 1 (lambda (x) (+ x 1)) Giving a name to a function: val name = fn arg => body
ML functions The syntax for anonymous functions in ML is: fn arg => body fn x => x + 1 (lambda (x) (+ x 1)) Giving a name to a function: val name = fn arg => body val inc = fn x => x + 1
ML functions The syntax for anonymous functions in ML is: fn arg => body fn x => x + 1 (lambda (x) (+ x 1)) Giving a name to a function: val name = fn arg => body val inc = fn x => x + 1 (define inc (lambda (x) (+ x 1)))
ML functions The syntax for anonymous functions in ML is: fn arg => body fn x => x + 1 (lambda (x) (+ x 1)) Giving a name to a function: val name = fn arg => body val inc = fn x => x + 1 (define inc (lambda (x) (+ x 1)))
37 ML functions The syntax for anonymous functions in ML is: fn arg => body fn x => x + 1 (lambda (x) (+ x 1)) Giving a name to a function: val name = fn arg => body val inc = fn x => x + 1 (define inc (lambda (x) (+ x 1))) Or: fun name arg = body
ML functions The syntax for anonymous functions in ML is: fn arg => body fn x => x + 1 (lambda (x) (+ x 1)) Giving a name to a function: val name = fn arg => body val inc = fn x => x + 1 (define inc (lambda (x) (+ x 1))) Or: fun name arg = body fun inc x = x + 1
ML functions The syntax for anonymous functions in ML is: fn arg => body fn x => x + 1 (lambda (x) (+ x 1)) Giving a name to a function: val name = fn arg => body val inc = fn x => x + 1 (define inc (lambda (x) (+ x 1))) Or: fun name arg = body fun inc x = x + 1 (define (inc x) (+ x 1))
ML functions The syntax for anonymous functions in ML is: fn arg => body fn x => x + 1 (lambda (x) (+ x 1)) Giving a name to a function: val name = fn arg => body val inc = fn x => x + 1 (define inc (lambda (x) (+ x 1))) Or: fun name arg = body fun inc x = x + 1 (define (inc x) (+ x 1))
ML functions The syntax for anonymous functions in ML is: fn arg => body fn x => x + 1 (lambda (x) (+ x 1)) Giving a name to a function: val name = fn arg => body val inc = fn x => x + 1 (define inc (lambda (x) (+ x 1))) Or: fun name arg = body fun inc x = x + 1 (define (inc x) (+ x 1)) In ML every function accepts exactly one argument.
ML functions The syntax for anonymous functions in ML is: fn arg => body fn x => x + 1 (lambda (x) (+ x 1)) Giving a name to a function: Or: val name = fn arg => body val inc = fn x => x + 1 (define inc (lambda (x) (+ x 1))) fun name arg = body fun inc x = x + 1 (define (inc x) (+ x 1)) In ML every function accepts exactly one argument.this argument could be a tuple.
38 ML types The type of a function is determined by the type of the input and the type of the output. Some examples: - fun inc x = x + 1; val inc = fn : int -> int - fun addinc(x,y) = x + y + 1; val addinc = fn : int * int -> int - fun optinc(x,y,c) = if c then x + y + 1 else x + y; val optinc = fn : int * int * bool -> int
39 parametric polymorphism What is the type of fn x => x?
parametric polymorphism What is the type of fn x => x? - fun id x = x; val id = fn : a -> a - id 324; val it = 324 : int - id 3.14; val it = 3.14 : real - id "foo"; val it = "foo" : string - id [1,2,3]; val it = [1,2,3] : int list - id (fn x => x + 1); val it = fn : int -> int
- fun id x = x; val id = fn : a -> a parametric polymorphism The a in ML stands for α. It is a type variable.
- fun id x = x; val id = fn : a -> a parametric polymorphism The a in ML stands for α. It is a type variable. id is a polymorphic function.
- fun id x = x; val id = fn : a -> a parametric polymorphism The a in ML stands for α. It is a type variable. id is a polymorphic function. α α means for every valid type α, α α α α α
- fun id x = x; val id = fn : a -> a parametric polymorphism The a in ML stands for α. It is a type variable. id is a polymorphic function. α α means for every valid type α, α α α α α When id is applied to 324, the type variable α is instantiated to int.
Examples: parametric polymorphism fun choose (a,b,c) = if a then b else c;
Examples: parametric polymorphism fun choose (a,b,c) = if a then b else c; choose : bool * a * a -> a;
Examples: parametric polymorphism fun choose (a,b,c) = if a then b else c; choose : bool * a * a -> a; fun swap (x,y) = (y,x);
Examples: parametric polymorphism fun choose (a,b,c) = if a then b else c; choose : bool * a * a -> a; fun swap (x,y) = (y,x); swap : a * b -> b * a;
parametric polymorphism What is the type of the following function? fun length lst = if (null lst) then 0 else 1 + length (tl lst);
parametric polymorphism What is the type of the following function? fun length lst = if (null lst) then 0 else 1 + length (tl lst); length : a list -> int
parametric polymorphism What is the type of the following function? fun length lst = if (null lst) then 0 else 1 + length (tl lst); length : a list -> int List is a polymorphic data type.
With named functions: currying - fun sum x y = x + y; val sum = fn : int -> int -> int - sum 2; val it = fn : int -> int - sum 2 3; val it = 5 : int
With anonymous functions: currying - fn x => fn y => x + y; val it = fn : int -> int -> int - (fn x => fn y => x + y) 2; val it = fn : int -> int - (fn x => fn y => x + y) 2 3; val it = 5 : int
pattern matching Value declaration (general form): val <pat> = <exp> - val mytuple = ("foo", "bar"); val mytuple = ("foo","bar") : string * string - val (x,y) = mytuple; val x = "foo" : string val y = "bar" : string - val mylist = [1,2,3,4]; val mylist = [1,2,3,4] : int list - val h::r = mylist; stdin:52.5-52.18 Warning: binding not exhaustive h :: r =... val h = 1 : int val r = [2,3,4] : int list 45
pattern matching is don t care : matches everything, binds nothing - val mytuple = ( foo, bar ); val mytuple = ("foo","bar") : string * string - val (first, _) = mytuple; val first = "foo" : string - val h::_ = [1,2,3]; stdin:66.5-66.19 Warning: binding not exhaustive h :: _ =... val h = 1 : int
pattern matching Function declaration with pattern matching: fun <name> <pattern1> = <exp1> <name> <pattern2> = <exp2>.. <name> <patternn> = <expn>; This means: the function name is name. It takes one argument (as any other ML function). It tries to match the argument to pattern1. If it succeeds, it returns value of exp1. Otherwise, tries to match the argument to pattern2. Etc, etc.
pattern matching For example we can (and should!) rewrite len to use pattern matching: fun len [] = 0 len (x::xs) = 1 + len xs; len : a list -> int Non-exhaustive patterns: - fun len (x::xs) = 1 + len xs; stdin... Warning: match nonexhaustive x :: xs =>... val len = fn : a list -> int - len [1,2,3]; uncaught exception nonexhaustive match failure raised at:...
pattern matching Function firstlist takes a list of pairs and returns the list consisting of the first elements only. For example: firstlist [] ==> [] firstlist [(1,2),(1,3)] ==> [1,1] firstlist [(1,"a"),(2,"b"),(3,"c")] ==> [1,2,3] firstlist [([],"a"),([1],"b"),([1,2],"c")] ==> [[],[1],[1,2]] fun firstlist [] = [] firstlist ((e1,e2)::es) = e1::(firstlist es); firstlist : ( a * b) list -> a list
Syntax of an ML let expression: let <declaration> <declaration>... in <expression> end ML notes
Example: ML notes fun reverse lst = let fun reverseacc [] acc = acc reverseacc (x::xs) acc = reverseacc xs (x::acc) in reverseacc lst [] end; reverse : a list -> a list - reverse [1,2,3]; val it = [3,2,1] : int list
52 ML notes Function application is left-associative: Example: - reverse 1::[2,3]; f g h == (f g) h... Error: operator and operand don t agree operator domain: Z list operand: int in expression: reverse 1 - reverse (1::[2,3]); val it = [3,2,1] : int list
-val i = 3 : count; val i = 3: count type synonyms We can give existing types new names. Syntax: type new type = ty new type becomes an alias (a synonym) for the existing type ty. -type float = real; type float = real -type count = int and average = real; type count = int type average = real -val f : float = 2.3; val f = 2.3: float
54 type synonyms But notice float, real, and average are all of the same base type, i.e. real -val f : float = 2.3; val f = 2.3 : float -val a = f : average; val a = 2.3 : average -val sum = a+f; val sum = 4.6 : average -val sum = f+a; val sum = 4.6 : float
55 user defined datatypes General Syntax: datatype new_type = Cons1 of type1 Cons2 of type2... ConsN of typen Defines a new type called new type. type1,...,typen are previously defined types. Cons1,...,ConsN are constructors. They are used to create a value of new type type. of type is omitted if a constructor does not need any argument (such constructors are called constants).
56 enumerated types All constructors are constants (no argument). Example: -datatype color = Red Blue Green; datatype color = Blue Green Red -val c = Red; val c = Red : color -fun colorstr Red = "Red" colorstr Blue = "Blue" colorstr Green= "Green"; val colorstr = fn : color -> string -colorstr c; val it = "Red" : string
57 Create union of different types: variant types datatype number = R of real I of int; val n1 = I 2; val n2 = R 3.0; val lst = [R 2.2, I 3, I 4, R 0.1]; (* val lst = [R 2.2,I 3,I 4,R 0.1] : number list *)
variant types datatype number = R of real I of int; val lst = [R 2.2, I 3, I 4, R 0.1]; fun sumints [] = 0 sumints ((I x)::rest) = x + sumints rest sumints ((R x)::rest) = sumints rest; sumints : number list -> int; sumints lst; (* val it = 7 : int *)
recursive types A datatype can be recursive: e.g. a linked list datatype llist = Nil Node of int * llist; (* datatype llist = Nil Node of int * llist *) val x = Nil; (* val x = Nil : llist *) val y = Node (5,Nil); (* val y = Node (5,Nil) : llist *) val z = Node(3, Node(2, Node(1,Nil))); (* val z = Node (3,Node (2,Node #)) : llist *)
recursive types datatype llist = Nil Node of int * llist; (* datatype llist = Nil Node of int * llist *) (* length of a linked list *) fun len Nil = 0 len (Node (_,xs)) = 1 + len xs; len : llist -> int len z; (* val it = 3 : int*)
recursive types What about a polymorphic linked list? datatype a llist = Nil Node of a * ( a llist); (* datatype a llist = Nil Node of a * a llist *) val y = Node (5,Nil); (* val y = Node (5,Nil) : int llist *) val z = Node("A", Node("B",Nil)); (* val z = Node ("A",Node ("B",Nil)) : string llist *)
recursive types datatype a llist = Nil Node of a * ( a llist); (* datatype a llist = Nil Node of a * a llist *) fun len Nil = 0 len (Node (_,xs)) = 1 + len xs; len : a llist -> int len y; (* val it = 1 : int *) len z; (* val it = 2 : int *)
recursive types Example: Tree representation of simple mathematical expressions. ( 3 + 2) + (( 1) + 4) 7) add add mult abs 2 add 7 neg neg 4 3 What is the datatype we need? 1 63
The datatype: recursive types datatype math_tree = Leaf of int Unary of (int -> int) * math_tree Binary of (int * int -> int) * math_tree * math_tree
The tree in the figure: recursive types val t = Binary(op +, Binary(op +, Unary((fn x => if x > 0 then x else ~x), Unary (op ~, Leaf 3)), Leaf 2), Binary(op *, Binary(op +, Unary(op ~, Leaf 1), Leaf 4), Leaf 7))
Evaluating the tree: recursive types (* evaluate the math_tree *) fun eval (Leaf n) = n eval (Unary (f,t)) = f (eval t) eval (Binary (f,l,r)) = f (eval l,eval r); eval : math_tree -> int - eval t; val it = 26 : int
SML notes mutual recursion Let s mimic the Scheme definitions of even and odd: fun even 0 = true even x = odd (x - 1); fun odd 0 = false odd x = even (x - 1); Error: unbound variable or constructor: odd
68 Use the keyword and. fun even 0 = true even x = odd (x - 1) and odd 0 = false odd x = even (x - 1); even : int -> bool; odd : int -> bool mutual recursion even 42; (* val it = true : bool *) odd 42; (* val it = false : bool *)
69 mutually recursive types Example: a tree with labeled branches: 1 6 2 3 7 8 4 5 datatype a tree = Empty Node of a branch * a branch and a branch = Branch of a * a tree;
The tree in the figure: mutually recursive types val lt = Node(Branch(1, Node(Branch(2, Empty), Branch(3, Node(Branch(4, Empty), Branch(5, Empty))))), Branch(6, Node(Branch(7, Empty), Branch(8, Empty))));
mutually recursive types datatype a tree = Empty Node of a branch * a branch and a branch = Branch of a * a tree Return the list of branch labels, in order: fun listtree Empty = [] listtree (Node (l,r)) = (listbranch l) @ (listbranch r) and listbranch (Branch (b,t)) = b :: (listtree t); listtree : a tree -> a list; listbranch : a branch -> a list listtree t; (* val it = [1,2,3,4,5,6,7,8] : int list *)
72 recursive types 1. A powerful tool for constructing new types. 2. The structure of the datatype suggests the structure of the recursive function on the datatype.
recursive types Recall our CFG for arithmetic expressions: <expn> --> <expn> + <expn> <expn> - <expn> <expn> * <expn> <expn> / <expn> <identifier> <literal> Let s define a (somewhat) corresponding datatype: datatype expn = Plus of expn * expn Minus of expn * expn Times of expn * expn Divn of expn * expn Num of real;
recursive types datatype expn = Plus of expn * expn Minus of expn * expn Times of expn * expn Divn of expn * expn Num of real; For example: 5 + 7*(-2) - 8 / 4 val e = Minus(Plus(Num 5.0, Times(Num 7.0, Num ~2.0)), Divn(Num 8.0, Num 4.0));
Evaluating the expression: recursive types fun eval (Num n) = n eval (Divn (x,y)) = (eval x) / (eval y) eval (Times (x,y)) = (eval x) * (eval y) eval (Minus (x,y)) = (eval x) - (eval y) eval (Plus (x,y)) = (eval x) + (eval y); eval : expn -> real; eval e; (* val it = ~11.0 : real *)
76 SML types, types, types... built-in types type synonyms, user-defined types enumerated, variant, union, recursive, mutually recursive recursion, higher-order functions (maps, folds)
using let in ML fun sumcube n = let fun cube x = x * x * x in if n = 0 then 0 else cube (n) + sumcube (n-1) end; Is it a good idea? cube is redefined every recursive call.
Better idea: using let in ML fun sumcube n = let fun cube x = x * x * x; fun sumc 0 = 0 sumc m = cube m + sumc (m-1) in sumc n end;
79 scope Name resolution: Given the use of a name (variable or function name), which instance of the entity with that name is referred to? Each use of a name must be associated with a single entity at run-time (i.e., an offset within a stack frame). The scope of a declaration of a name is the part of the program in which a use of that name refers to that declaration. The design of a language includes scope rules for resolving the mapping from the use of each name to its appropriate declaration.
80 A name is: scope visible to a piece of code if its scope includes that piece of code. local to a piece of code (block/ procedure/main program) if its declaration is within that piece of code. non-local to a piece of code if it is visible, but its declaration is not within that piece of code. A declaration of a name is hidden if another declaration supersedes it in scope.
scope example program L; var n: char; {n declared in L} procedure W; begin write(n); {n referenced in W} end; procedure D; var n: char; {n declared in D} begin n:= D ; {n referenced in D} W end; begin n:= L ; {n referenced in L} W; D
82 lexical scope Names are associated with declarations at compile time. Find the smallest block syntactically enclosing the reference and containing a declaration of the name. Example: The reference to n in W is associated with the declaration of n in L. The output is? Also called static scope.
83 dynamic scope Names are associated with declarations at run time. Find the most recent, currently active run-time stack frame containing a declaration of the name. Example: The reference to n in W is associated with two different declarations at two different times. The output is?
scope Lexical scope is based on the principle that consistent renaming of variables should not effect the value of an expression. Lexical scope goes a long way to establishing referential transparency easy to determine where variables obtain their values, because program itself (unlike run-time contexts) is not changing. Most modern languages (including Scheme and ML) use lexical scope, although early interpreted languages (LISP) used dynamic scope because of the flexibility and ease of implementation.
exceptions 85
85 exceptions Many functions are partial, i.e. they are only defined for a subset of the function s domain type. Other values in the domain type may be treated as exceptions: e.g., f (x) = 1/x Exceptions are a control construct. They provide a structured form of jump to exit a construct such as a function invocation or a block.
85 exceptions Many functions are partial, i.e. they are only defined for a subset of the function s domain type. Other values in the domain type may be treated as exceptions: e.g., f (x) = 1/x Exceptions are a control construct. They provide a structured form of jump to exit a construct such as a function invocation or a block. Terminate part of computation: Jump out of construct. Pass data as part of jump. Return to most recent site set up to handle exception. Unnecessary activation records may be deallocated (may need to free heap space, other resources).
86 Consider the following functions: fun f x = 1.0 / x; fun g x = hd x; fun h x = tl x; exceptions Will the type-checker complain? Is there a problem?
87 SML exceptions ML s exceptions (similar to the ones in Java or Python) provide a uniform way to handle errors, and eliminate the need for ad hoc, special exceptional return values from functions. When encountering a special case, raise an exception. The caller will catch/handle the exception and will take care of it. Primitive exceptions: 3 div 0 (* raises Div *) hd nil (* raises Empty *)
SML user-defined exceptions Defining an exception: exception <name> of <type> Raising an exception: raise <name> <arg> exception factneg; fun robustfact n= let fun fact 0 = 1 fact n = n * fact (n-1); in if n < 0 then raise factneg else fact n end; robustfact 5; (* val it = 120 : int *) robustfact ~5; (* uncaught exception factneg *)
SML exceptions Exceptions can have arguments, just like datatypes: exception factneg of int; fun robustfact n= let fun fact 0 = 1 fact n = n * fact (n-1); in if n < 0 then raise factneg n else fact n end;
Handling exceptions: SML exceptions <expr> handle <match1> => <expr1> <match2> => <expr2>... <matchn> => <exprn> Any restrictions on types?
Example: SML exceptions fun printfact n = print (Int.toString(robustFact n)^"\n") handle factneg x => print ("printfact "^(Int.toString x)^ ": argument must be >= 0\n"); (* val printfact = fn : int -> unit *) printfact ~5; (* printfact ~5: argument must be >= 0 val it = () : unit *)
SML exceptions Exceptions are handled according to dynamic scoping. exception e1 and e2 and e3; fun h 1 = raise e1 h 2 = raise e2 h 3 = raise e3 h _ = "ok"; fun g(n) = h(n) handle e2 => "error g2" e3 => "error g3"; fun f(n) = g(n) handle e1 => "error f1" e2 => "error f2"; f(4); f(3); f(2); f(1); f(0);
93 General dynamic scoping rule: exception handling Jump to most recently established handler on run-time stack. Dynamic scoping is not an accident: User knows how to handler error. Author of library function does not.
exceptions Why do we need exceptions in a strongly typed language?
exceptions Why do we need exceptions in a strongly typed language? Type-checker only checks types of parameters, not their values. In languages w/o exception handling, when an exception occurs, control goes to the OS and the program is terminated, or special code must be written to handle exceptions (e.g., pass special parameter or use return value of procedure to indicate status of program, etc.) In contrast, with exception handling, programs can fix the problem and continue, if desirable. Some uses: (see examples in Mitchell 8.2) Error handling (when no reasonable value can be returned). Efficiency (we don t like this one). Two types of exceptions in SML: Built-in exceptions. User-defined exceptions.
immutable data Variables (and data structures), once created and initialized, are immutable: they are never changed, updated, or stored into. Powerful guarantees of noninterference. Build new data structures (and let the old ones be garbage collected) instead of modifying old ones.
96 references functional programming no side effects ML separates pure expressions and side effects. Assignment is restricted to reference cells, which are of a different type. Assignment restrictions are enforced by the type system.
references Syntax: ref v creates a reference cell with value v!r returns contents of reference cell r r:=v sets contents of reference cell r to value v val x = ref 0; (* val x = ref 0 : int ref *) x; (* val it = ref 0 : int ref *)!x; (* val it = 0 : int *) x:=3; (* val it = () : unit *) x; (* val it = ref 3 : int ref *)!x; (* val it = 3 : int *) 97
references The type of a reference cell that contains value v of type α is α ref. Assignment to a reference cell must be consistent with the type of the reference cell Example: val x = ref "cscc24"; (* val x = ref "cscc24" : string ref *) x:=0; (* Error: operator and operand don t agree operator domain: string ref * string operand: string ref * int in expression: x := 0 *)
references (* abslst = fn : int ref list -> unit *) fun abslst [] = () abslst (x::rest) = (x:=abs(!x); abslst rest); val L = [ref ~1, ref 0, ref ~2]; (* val L = [ref ~1,ref 0,ref ~2] : int ref list*) abslst L; (* val it = () : unit *) L; (* val it = [ref 1,ref 0,ref 2] : int ref list *)
references (* abslst = fn : int list -> int list *) fun abslst [] = [] abslst (x::rest) = (abs x)::(abslst rest); fun abslst lst = map abs lst; val L = [~1, 0, ~2]; (* val L = [~1,0,~2] : int list *) abslst L; (* val it = [1,0,2] : int list *) L; (* val it = [~1,0,~2] : int list *)
101 ML functional type safe static type checking type inference polymorphic abstract data types exception handling static scope formal definition garbage collection immutable data types updatable references