CSE341 Notes for Monday, 4/5/10

I first talked about ML's pattern matching facility. ML has the ability to match certain forms of expressions. For example, previously we bound a single variable to a tuple, as in

        val x = (3.4, "hello");
but ML allows you to define something that looks like a tuple on the left side with the actual tuple on the right:

        val (x, y) = (3.4, "hello");
This binds x to 3.4 and y to "hello". ML can even do this with lists. For example, if we say:

        val [x]  = [3];
ML will bind x to 3. In this case we get a warning about the matches not being exhaustive. The warning is more useful when we're writing functions. It's just letting us know that we aren't using every possible kind of list here. We can also match one-element lists:

        val x::xs = [1, 3, 5];
which binds x=1 and y=[3, 5]. Or we can bind a two-element list:

        val x::y::zs = [1, 3, 5];
which binds x=1, y=3, zs=[5].

Using pattern matching, we looked at how to write functions that specify their result through a series of cases. For example, we know that the Fibonacci sequence begins with the values 1, 1 and that each subsequent value is the sum of the previous two. We could write this with an if/else:

        fun fib(n) =
            if n = 1 orelse n = 2 then 1
            else fib(n - 1) + fib(n - 2);
but we can also write this as a series of three cases, each with a different pattern:

        fun fib(0) = 1 | fib(1) = 1 | fib(n) = fib(n - 1) + fib(n - 2);
Notice the two pipe or vertical bar characters ("|") that separate the three different cases. We usually write this with each case on a separate line and we line up the pipe characters with the keyword "fun", as in:

        fun fib(0) = 1
        |   fib(1) = 1
        |   fib(n) = fib(n - 1) + fib(n - 2);
Then I asked how to write a function to return the values from a list that appear in odd positions. I said that we'd assume that we are using one-based indexing, so the values in odd positions are the first, third, fifth, and so. Using patterns, we wrote the following function for odds:

        fun odds([]) = []
        |   odds([x]) = [x]
        |   odds(x::y::zs) = x::odds(zs);
Then we turned to a related but tougher problem: how do we split a list into two lists: those in odd positions and those in even positions. Since it has to return two things, we'll assume it returns a tuple. For example, the call split([1, 8, 6, 4, 9]) should return ([1, 6, 9], [8, 4]). We included a case for the empty list:

        fun split([]) = ([], [])
and then I asked people to think about the general case where we have at least two values:

        fun split([]) = ([], [])
        |   split(x::y::zs) = ?
I asked how we write this. Someone suggested that we make a recursive call passing in zs. That seems like a good idea. But then what do we do with the result? The result is a tuple. That's not particularly easy to work with. We could store the tuple in a variable using let:

        fun split([]) = ([], [])
        |   split(x::y::zs) = 
                let val result = split(zs)
                in ?
                end;
This would work, but we'd still have to use functions like #1 and #2 to pull apart the result. We can do even better by using a pattern that mentions the two parts of the tuple:

        fun split([]) = ([], [])
        |   split(x::y::zs) = 
                let val (M, N) = split(zs)
                in ?
                end;
This will recursively split the list and then bind the variables M and N to be the two parts of the resulting tuple. The overall result in this case is a new tuple that puts x at the front of M and y at the front of N:

        fun split([]) = ([], [])
        |   split(x::y::zs) = 
                let val (M, N) = split(zs)
                in (x::M, y::N)
                end;
When we tried to load this into the interpreter, we got the warning that the matches are not exhaustive. I said that for functions, you want to pay close attention to this. Maybe it's okay because you have a precondition that a certain case won't happen, but then be sure you've thought about it. In this case, when we tried to split a list, we got an error:

        uncaught exception Match [nonexhaustive match failure]
The problem is that we need a case in the original for a one-element list:

        fun split([]) = ([], [])
        |   split([x]) = ([x], [])>
        |   split(x::y::zs) = 
                let val (M, N) = split(zs)
                in (x::M, y::N)
                end;
This version of the function worked fine.

Then we talked about how to write a function that would return the list obtained by merging two sorted lists into one sorted list. We had base cases for one or the other list being empty:

        fun merge(L, []) = L
        |   merge([], M) = M
        ...
We considered a case where both lists are empty, but we concluded that the first case takes care of that (actually either case takes care of it, but the first case is the one that will end up handling it). We then considered the case where each list has at least one value:
        fun merge(L, []) = L
        |   merge([], M) = M
        |   merge(x::xs, y::ys) =
        ...
Someone said we test whether y is greater than x. Given that test, we either put x or y at the front of the answer:

        fun merge(L, []) = L
        |   merge([], M) = M
        |   merge(x::xs, y::ys) =
                if y > x then x::merge(xs, y::ys)
        	else y::merge(x::xs, ys)
This version of the function worked fine, but it seems a bit awkward to have to compute "x::xs" or "y::ys" in making the recursive call. ML gives an alternative with an "as" clause:

        fun merge(L, []) = L
        |   merge([], M) = M
        |   merge(L as x::xs, M as y::ys) =
                if y > x then x::merge(xs, M)
        	else y::merge(L, ys)
Then we talked about how to implement the merge sort algorithm. As usual, we have an empty list case:

        fun mergeSort([]) = []
        ...
In the general case, we split the list, sort the two sublists and then merge the two sorted lists. We used a val declaration to introduce variables for the two lists that come back from a call on split:

        fun mergeSort([]) = []
        |   mergeSort(lst) =
                let val (M, N) = split(lst)
                in ...
                end
If you think procedurally, you might think of it as three more steps:

  1. sort the first sublist
  2. sort the second sublist
  3. merge the two together
Ullman seems to be thinking that way because he includes two extra val declarations:

        fun mergeSort([]) = []
        |   mergeSort(L) =
                let val (M, N) = split(L)
                    val list1 = mergeSort(M)
                    val list2 = mergeSort(N)
                in merge(list1, list2)
                end
I think it's better to express this in a more functional way using a single expression:

        fun msort([]) = []
        |   msort(L) =
                let val (M, N) = split(L)
        	in merge(msort(M), msort(N))
        	end
We tried running this version of the code and found that it didn't work. It went into infinite recursion. The problem is that eventually you will get down to a 1-element list and the split function returns a tuple with the same 1-element list along with an empty list. So we end up recursively sorting the same 1-element list over and over. For this function, we need a special case for the 1-element list:

        fun msort([]) = []
        fun msort([x]) = [x]
        |   msort(L) =
                let val (M, N) = split(L)
        	in merge(msort(M), msort(N))
        	end
This version worked fine.


Stuart Reges
Last modified: Tue Apr 6 17:34:05 PDT 2010