[Notes by Jeff Vaughan (part 2), Tibor Janosi (part 1, 3)] 1. signature MYSIG = sig type secret val zero: secret val one: secret val sum: secret * secret -> secret val myprint: secret -> unit end; A signature is just a "public contract" that is advertised by a structure. Signatures and structures can be used to (a) hide implementation details from outside users; (b) enforce a predefined type for each publicly accessible value in a structure. There are many other issues here, but we will not need to discuss these at this stage of the course. structure MYSTRUCT: MYSIG = struct type secret = int val zero = 0 val one = 1 fun sum (x: secret, y: secret): secret = x + y fun myprint(x: secret): unit = print(Int.toString(x)) end Such a structure is not opaque, due to the use of the ":" operator to associate the signature to the structure. Thus in this case an outside user would be able to use "secret" as an alias for int, and would also know that "zero" is in fact 0:int. structure MYSTRUCT:> MYSIG = struct ... end; Due to the use of the ":>" operator the structure now becomes opaque. The type "secret" is an abstract type whose inner structure is not accessible outside of the structure. All we can know about the structure is the information explicitly given in the signature. ---------------------------------------------------------------------- 2. The biggest topic in this class now is folding, and I think we should devote some time to it. Although Ramin discussed this in class we should show the examples: foldl (fn (x: int, y: int list) => x::y) [] [1, 2, 3, 4] (* [4,3,2,1] *) foldr (fn (x: int, y: int list) => x::y) [] [1, 2, 3, 4] (* [1,2,3,4] *) We can help them develop some intuition with the "backpack" model. Folding on a list is like walking down the list with a backpack. At every element you apply the function to the current element and the element in you backpack. The result of this function is placed in backpack and you step to the next location. In the foldl case above, you begin with [] in the back. First step puts you at 1, remove [] from your bag, evaluate (fn()=>...) (1,[]) to get [1], put [1] in your backpack. Second step is the same, but you put [2, 1] in the bag. The same model works for foldr, but you start at the far (i.e. right) end of the list. The code for foldl is fun foldl (f: ('a*'b) -> 'b) (acc:'b) (x:'a list):'b = case x of [] => acc | hd::tl => foldl f (f (hd, acc)) tl and foldr is fun foldr (f: ('a*'b) -> 'b) (acc:'b) (x:'a list):'b = case x of [] => acc | hd::tl => f(hd,(foldr f acc tl)). Note that foldl is implemented by a tail recursive function, while foldr is not tail-recursive. For the moment assume foldl has the type ('a*'b->'c)->'d->'e list -> 'f can we narrow this down? Yes. Note that in the in empty list case we return base. Therefore 'd = 'f. Furthermore we pass base as the second parameter to f. Therefore 'b = 'd = 'f. This gives us: ('a*'b->'g)->'b->'e list -> 'b. In the recursive call the return value of f is used as base. Therefore 'g = 'b. ('a*'b->'b)->'b->'e list -> 'b The last inference we need to make is that because the head of x is passed to f, 'e = 'a. ('a*'b->'b)->'b->'a list -> 'b And we've worked out the type form the code. ---------------------------------------------------------------------- 3. Here are some more examples of recursive functions, pattern matching, and use of generic types: First, we define two operations: zip and unzip. zip([1, 2, 3], ["one", "two", "three"]) = [ (1, "one"), (2, "two"), (3, "three") ] Unzip is the reverse operation. These functions are implemented as part of the ListPair signature. fun zip (l1: 'a list, l2: 'a list): ('a * 'a) list = case (l1, l2) of ([], []) => [] | ([], _) => raise Fail "uneven length" | (_, []) => raise Fail "uneven length" | (h1::t1, h2::t2) => (h1, h2)::zip(t1, t2) We can do better by changing the order of the patterns we test for: fun zip (l1: 'a list, l2: 'a list): ('a * 'a) list = case (l1, l2) of ([], []) => [] | (h1::t1, h2::t2) => (h1, h2)::zip(t1, t2) | _ => raise Fail "lists have uneven length" Here is a tail-recursive version that uses a helper function: fun zip(l1: 'a list, l2: 'a list): ('a * 'a) list = let fun helper (l1: 'a list, l2: 'a list, acc: ('a * 'a) list): ('a * 'a) list = case (l1, l2) of ([], []) => rev acc | (h1::t1, h2::t2) => helper(t1, t2, (h1, h2)::acc) | _ => raise Fail "lists have different length" in helper (l1, l2, []) end Can we use foldl, foldr interchangeably? If yes, when? Should we use foldl or foldr with unzip? fun unzip (l: ('a * 'b) list): ('a list) * ('b list) = foldr (fn ((v1: 'a, v2: 'b), b: 'a list * 'b list) => (v1::(#1 b), v2::(#2 b))) ([], []) l - unzip []; stdIn:17.1-17.9 Warning: type vars not generalized because of value restriction are instantiated to dummy types (X1,X2,...) GC #0.0.0.0.1.12: (0 ms) val it = ([],[]) : ?.X1 list * ?.X2 list - unzip [(1,2)]; val it = ([1],[2]) : int list * int list - unzip [(1, 2), (3, 4)]; val it = ([1,3],[2,4]) : int list * int list - unzip [(1, "one"), (2, "two"), (3, "three")]; val it = ([1,2,3],["one","two","three"]) : int list * string list