CSE413 Notes for Monday, 1/8/24

I began by giving an example of a simple function that uses nested if/else expressions to produce one of three values. The function is called signum in mathematics and it returns either -1, 0, or 1 to indicate whether its argument is either negative, zero, or positive. In Java the Comparable interface uses a related idea of indicating one of three possible situations based on negative, zero, or positive integers.

I mentioned that I don't mind what identation scheme students use as long as they are consistent, but when I write a function like this, I like to see each case on a line by itself, as in:

        let signum(n) =
            if n < 0 then -1
            else if n = 0 then 0
            else 1
We then wrote a function called count_down that takes an integer n as a parameter and that returns a list of int values starting with n and counting down to 0. For example, count_down(10) should return [10; 9; 8; 7; 6; 5; 4; 3; 2; 1; 0]. I pointed out that our function won't work for negative values of n, so we should document that as a precondition of the function. I have asked students to include precondition comments for homework 1.

        (* pre: n >= 0 *)
        let rec count_down(n) =
            if n = 0 then [0]
            else n::count_down(n - 1)
Then I introduced two new OCaml concepts. First we looked at how a let construct can be used to create a local set of bindings that aren't introduced into the overall environment. It's similar to the idea of local variables in Java that appear inside of a block (i.e., inside a set of curly braces {}). For example, we might say:

        let x = 0.35 in x *. x *. x *. x
This is a convenient way to give the name x to the numeric value we want to use in this expression while also keeping that name local to just this expression. It simplifies the expression without introducing a binding for x in the global environment. The general form of the let construct is:

let <binding> in <expression> We could read it as, "Let the following binding hold in evaluating this expression." Most of the OCaml constructs we are studying evaluate to something. That is true of this new version of the let expression. For example, we can put parentheses around the let expression above and form more complex expressions, as in:

        (let x = 0.35 in x *. x *. x *. x) +. 2.5
We can't do the same thing with the other form of let which simply defines a new binding for the top level environment:

        (let x = 0.35) +. 2.5
This produces an error message because this form of let does not produce a value.

You can include a function binding as well as a variable binding in a let expression. Often we use a let to define a helper function for some other function.

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

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

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

        let [x]  = [3]
OCaml 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:

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

        let 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:

        let rec fib(n) =
            if 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:

        let rec fib(n) =
            match n with
            | 0 -> 1
            | 1 -> 1
            | n -> fib(n - 1) + fib(n - 2)
This function definition uses a match expression. Notice that it uses vertical bar characters to separate different possible matches. You can read it as, "Either a 0 or a 1 or n." The match expression has the following general syntax.

match <expression> with | <pattern> -> <expression> ... | <pattern> -> <expression> The first vertical bar character is optional. At runtime OCaml trys to find a pattern that matches the given expression and when it finds a match, it evaluates the corresponding expression and returns that value as the result. If no pattern matches, an exception is throw.

Then we wrote a function to find the minimum value in a list. I asked what would be the easiest list to find the minimum of and someone said a list of one element. And for a list of more than one element, we can test to see whether the first value in the list is smaller than the minimum of the rest of the list. So we started with this version:

        let rec min(lst) =
            match lst with
            | [x]   -> x
            | x::xs -> if x < min(xs) then x else min(xs)
We got this response from the OCaml interpreter:

        Warning 8: this pattern-matching is not exhaustive.
        Here is an example of a case that is not matched:
        []
This was only a warning, so it didn't prevent our code from running. When you have a match expression, OCaml will let you know if your patterns are not exhaustive. In this case, it is letting us know that we missed the case of an empty list. For an empty list, we really can't compute a minimum. There are several options for how to handle this, but the easiest thing to do right now is to introduce a precondition and to raise an exception if the precondition is not satisfied. We do so by calling the invalid_arg function passing it a string to display.

        (* pre: lst is not empty *)
        let rec min(lst) =
            match lst with
            | []    -> invalid_arg("empty list")
            | [x]   -> x
            | x::xs -> if x < min(xs) then x else min(xs)
With this extra case, we no longer got a warning. But this version of the function turns out to be very inefficient. I tried asking the interpreter to evaluate:

        min(count_down(30))
it eventually figured out that the minimum is 0, but it took a long time to compute. That's because the min function potentially recomputes the recursive minimum twice. Giving it a list in reverse order guarantees that it will always call it twice. Think of the final branch as saying, "If the first value is less than the minimum of the rest of the list (which it never will be in this case) then return the minimum of the rest of the list."

Consider this case where the list has 30 elements. To compute its minimum, we twice compute the minimum of the 29-element list that is the tail of this list. Each of those two calls requires two calls to find the minimum of the 28-element list that is tail of that list. There is a doubling going on where each recursive call is done twice as often as the one before. So this requires something on the order of 230 which is approximately 109 or a billion. No wonder it is taking a long time.

So how do we solve this problem? Someone asked if we can compute the recursive minimum once and store it in a variable. That's exactly what the let expression allows you to do:

        (* pre: lst is not empty *)
        let rec min(lst) =
            match lst with
            | []    -> invalid_arg("empty list")
            | [x]   -> x
            | x::xs -> let m = min(xs) 
                       in if x < m then x else m
This version did not produce any warnings and worked well even when we asked for min(count_down(1000)).

Then we discussed how to write functions to return the values from a list 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:

        let rec odds(lst) =
            match lst with
            | []       -> []
            | [x]      -> [x]
            | x::y::zs -> x::odds(zs)
As a final example, I talked about writing a split function that would 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]). Someone said that we could modify our odds function and write a similar evens function and then define split as follows:

        fun split(lst) = (odds(lst), evens(lst));
That would work, but it would be inefficient. It would scan through the list twice when it only needs to do so once. So we started to write it from scratch. We included a case for the empty list:

        let rec split(lst) =
            match lst with
            | [] -> ([], [])
and then I asked people to think about the general case where we have at least two values:

        let rec split(lst) =
            match lst with
            | []       -> ([], [])
            | 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:

        let rec split(lst) =
            match lst with
            | []       -> ([], [])
            | x::y::zs -> let result = split(zs)
                          in ?
This would work, but we'd still have to pull apart the resulting tuple. We can do even better by using a pattern that mentions the two parts of the tuple:

        let rec split(lst) =
            match lst with
            | []       -> ([], [])
            | x::y::zs -> let (lst1, lst2) = split(zs)
                          in ?
This will recursively split the list and then bind the variables lst1 and lst2 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 lst1 and y at the front of lst2:

        let rec split(lst) =
            match lst with
            | []       -> ([], [])
            | x::y::zs -> let (lst1, lst2) = split(zs)
                          in (x::lst1, y::lst2)
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 when we tried split a list of odd length:

        # split([1; 2; 3]);;
        Exception: Match_failure ("//toplevel//", 2, 12).
The problem is that we need a case in the original for a one-element list. In fact, OCaml had told us as much when it said that we had overlooked the possible case of "_::[]". The underscore (_) is a wildcard so this expresion would be equivalent to [_], which is a way of describing the one element list. So we added a case for that:

        let rec split(lst) =
            match lst with
            | []       -> ([], [])
            | [x] -> ([x], [])
            | x::y::zs -> let (lst1, lst2) = split(zs)
                          in (x::lst1, y::lst2)
This version of the function worked fine.

I mentioned that the function we wrote for computing the Fibonacci sequence is also inefficient. Recall that we wrote it this way:

        let rec fib(n) =
            match n with
            | 0 -> 1
            | 1 -> 1
            | n -> fib(n - 1) + fib(n - 2)
As with our min function, this one recomputes many values. In the general case (the last one), it computes a Fibonacci number by computing two previous ones, both of will tend to require computing two previous ones, and so on. It isn't quite as inefficient as min, but it's close (it's actually an exponential that is a slight variation of the Fibonacci sequence itself).

Here we need something extra. The key is to keep track of two values from the sequence that you update over and over. Our function has only one parameter, so it won't allow us to keep track of a changing pair of values. The solution is to introduce a helper function. We can pass it a pair of values and an indication of how many more computations to perform. We'd still have a base case for n of 1 or 2, in which case we return 1 (because the sequences starts with two 1s). But we can allow the helper function to compute the remaining values in the sequence:

        let fib(n) = 
            if n <= 2 then 1
            else fib_helper(n - 2, 1, 1)
For the helper function, we can compute pairs of values from the sequence the appropriate number of times, and when we get down to 0 computations left, we can return the computed value from the sequence:

        let rec fib_helper(n, fib1, fib2) =
            match n with
            | 0 -> fib2
            | n -> fib_helper(n - 1, fib2, fib1 + fib2)
This version can efficiently compute the result even for large values of n. We can do even better by hiding the helper function inside of the fib function with a let expression:

        let fib(n) =
            let rec fib_helper(n, fib1, fib2) =
                match n with
                | 0 -> fib2
                | n -> fib_helper(n - 1, fib2, fib1 + fib2)
            in if n <= 2 then 1
               else fib_helper(n - 2, 1, 1)

Stuart Reges
Last modified: Tue Feb 13 10:33:52 PST 2024