CS312 Fall 2004 Section Notes #3 Written up by Harlan Crystal Objective: Lists & List operators, recursion, data carrying data types, anonymous functions, map & fold. Clearing up some issues with binding anonymous functions ----------------------------------------------------------- There was some confusion on the previous homework about the relationship between fn and fun. For most purposes, the following are equivalent: fun f(x:int):int = 3 val f:int->int = fn x => 3 There are some slight differences which we won't go into (ie how would you write a recursive function with the second syntax?) So when the homework asked for a tuple of type (int * (int -> int)), both of the following would have been valid answers: (3, fn x => x + 1) OR fun f(x:int):int = x + 1 (3, f) List operations --------------------- Last section we spoke about simple list operations such as cons, cat, hd, tl and null. One more useful function is rev, which simply reverses a list. Here are some examples of it in action: rev([1,2]) =====> [2, 1] rev(["hello"]) =====> ["hello"] rev([]) =====> [] rev(rev([2, 3, 4])) =====> [2, 3, 4] Another useful list function is length, which simply returns the length of said list: length([]) =====> 0 length([(1, 2.0), (5, 3.5)]) =====> 2 Let's implement our own version of list reversal: fun myrev(l:int list):int list = let fun inner(oldlist:int list, newlist:int list):int list = case oldlist of [] => newlist | x::xs => inner(xs, x::newlist) in inner(l, []) end Deep Pattern Matching -------------------------- One useful feature of pattern matching is that it can match deep into the structure. For example, consider the following code function which takes an int list list and sums all their values: fun sumlistlist(l:int list list):int = case l of [] => 0 | x::xs => (case x of [] => sumlistlist(xs) | y::ys => y + sumlistlist(ys::xs)) In most situations, case expressions within case expressions can and should be avoided. This could be written more cleanly as: fun sumlistlist(l:int list list):int = case l of [] => 0 | []::xs => sumlistlist(xs) | (y::ys)::xs => y + sumlistlist(ys::xs) Now we can use this function to do the following: sumlistlist([]) ======> 0 sumlistlist([[]]) ======> 0 sumlistlist([[1], [2]]) ======> 3 sumlistlist([[1, 2, 3], [4, 5, 6], []]) ======> 21 Another example of deep pattern matching would be with lists of datatypes. Let's imagine the following data-carrying datatype: datatype number = INT of int | REAL of real Let's write a function which would sum a list of these such as [INT(1), REAL(4.5), INT(~4)] fun sumnumlist(l:number list):real = case l of [] => 0.0 | INT(x)::ys => Int.toReal(x) + (sumnumlist(ys)) | REAL(x)::ys => x + (sumnumlist(ys)) More list operations ----------------------------------- One useful function is mapping some function across all the elements of a list to create another list. For example, some nice mappings would be: [1, 2, 3, 4] ====~===> [2, 4, 6, 8] [1, 2, 3, 4] ====~===> [2, 3, 4, 5] [1, 2, 3, 4] ====~===> [1, 4, 9, 16] [2.0, 3.4, 6.5] ====~===> [1.0, 2.4, 5.5] ["hi", "bye"] ====~===> ["HI", "BYE"] This can be done with the map function. Given a mapping function which it applies to each element, the map function creates a one-to-one mapping to another list. This means that the lists will have the same number of elements, and the processing of each element is independent. Let's see it in action: map (fn x => x * 2) [1, 2, 3, 4] =======> [2, 4, 6, 8] map (fn x => x + 1) [1, 2, 3, 4] =======> [2, 3, 4, 5] map (fn x => x * x) [1, 2, 3, 4] =======> [1, 4, 9, 16] map (fn x => x - 1.0) [2.0, 3.4, 6.5] =======> [1.0, 2.4, 5.5] map String.toUpper ["hi", "bye"] =======> ["HI", "BYE"] Ignore the strange syntax used to invoke map (just listing the parameters in a row without parenthesis.) This is what one is called a curried function which will be discussed in the next section. Let's write our own implementation of map to show that we understand it: fun mymap(f:int->int, l:int list):int list = case l of [] => [] | x::xs => (f(x))::mymap(f, xs) One thing about our implementation of map is that it can only operate on int lists. Notice that the version of map in the library can work on real lists or any other type of list. Furthermore, the built-in version of map does not require that the mapping function produces the same output type as the input type. For example: map Int.toString [1, 2, 3] ======> ["1", "2", "3"] In the next lecture you will learn about parametric polymorphism, which allows one to write a version of map which can operate on varying types in this way. Another useful function is foldl. This function allows extremely powerful list processing, but for now we will just present a few examples of simple operations using it: foldl (fn (x:int, s:int) => x + s) 0 [1, 2, 3, 4] ~~~~> 0 + 1 + 2 + 3 + 4 foldl (fn (x:int, p:int) => x * s) 1 [1, 2, 3, 4] ~~~~> 1 * 1 * 2 * 3 * 4 Within the context of these simple examples, you can see fold as applying an operation across the list, using the extra argument as a starting point. We will speak more about the more powerful features of fold in the next section.