CSE413 Notes for Friday, 1/5/24

I began by mentioning that it is important to understand the distinction between static and dynamic properties of code.
    static                     dynamic
    ----------------------------------------------
    before execution           during execution
    compilers (fast)           interpreters (slow)
I said, for example, that in Python if you define a function that takes two arguments called a and b and it returns the value a + b, Python doesn't check in advance that it makes sense to add a and b together. That means that it can lead to runtime errors. OCaml, by contrast, does extensive type checking before any code is allowed to run. That's why we describe OCaml as having static type checking.

I mentioned that I plan to build up a sort of cheat sheet for OCaml that lists the different language features we have discussed. I will update it for each assignment to include any new features that we are exploring in that homework. The first version is available here.

I pointed out that OCaml cares a lot about the types of various program elements, but it uses type inference to deduce the types when it can. For example, the inc function below doesn't need the type specification because of the use of the + operator and the int value 1:

        let inc(n) = n + 1
I tend not to include type specifications unless I need to.

Then we discussed the if/else construct, which has the following general form:

if <boolean expression> then <expression> else <expression> I wrote this simple function for finding the minimum of two values:

        let min(x, y) = if x < y then x else y
OCaml indicated that the parameters are of type 'a. This is OCaml's way of saying that it could be of any type. We call that a polymorphic type. But what if you wanted to tell OCaml to use the int version of comparison? We can do that by specifying a type and all it takes is one such specification and OCaml does the rest:

        let min((x:int), y) = if x < y then x else y
        let min(x, (y:int)) = if x < y then x else y
        let min(x, y) : int = if x < y then x else y
        let min(x, y) = if x < y then (x:int) else y
Then we talked about using recursion to define functions. I mentioned that the functional languages use recursion often, so if you haven't yet mastered it, this will be your chance to finally figure it out.

The first example we looked at is a factorial function:

        let rec factorial(n) =
            if n = 0 then 1
            else n * factorial(n - 1)
Notice that we had to begin the definition with "let rec" instead of a simple "let". This is required for recursive functions. We found that this function went into infinite recursion for a negative number. For the homework I have asked you to document these kind of preconditions, as in:

        (* pre: n >= 0 *)
        let rec factorial(n) =
            if n = 0 then 1
            else n * factorial(n - 1)
Then we discussed how to work with lists. You can construct a list using the operator :: which is known as the "cons" operator. For example, we constructed a list by starting with an empty list and inserting a 7 in front, and then a 5 in front of that, and then a 2 in front of that, and then a 45 in front of that:

        let lst = 45::2::5::7::[]
The cons operator evaluates from right to left, so we ended up with this list:

        [45; 2; 5; 7]
Then we looked at some examples of list recursion. We can pull a list apart by using the functions List.hd and List.tl. List.hd returns the "head" of the list (the first value). List.tl returns the "tail" of the list (the rest of the list). Using these functions, we wrote a classic recursive list function to find the sum of a list of ints:

        let rec sum(lst) =
            if lst = [] then 0
            else List.hd(lst) + sum(List.tl(lst))
Then we wrote a function that took a list of int values as a parameter and that returned a new list with each value incremented by one:

        let rec inc_all(lst) =
            if lst = [] then []
            else (List.hd(lst) + 1)::inc_all(List.tl(lst))
And we wrote a function to return the last value in a list, which had a different base case than the other examples:

        let rec last(lst) =
            if List.length(lst) = 1 then List.hd(lst)
            else last(List.tl(lst))
This version has a precondition that the list is not empty.

For a final example, I asked people to consider the problem of converting a list of names. We assume the names appear as a list of tuples with last name followed by first name, as in:

        let test = [("Clinton", "Hillary"); ("Obama", "Barak"); ("Biden", "Joe")]
The idea is to convert it into a list of simple strings where each string has the first name followed by a space followed by the last name:

        ["Hillary Clinton"; "Barack Obama"; "Joe Biden"]
We started a recursive definition:

        let rec convert(lst) =
            if lst = [] then []
            else ...
At this point we were left with the problem of taking the head of the list and converting it from one form to another. It's a little too much complexity for us to handle it all at once. This is a great place to introduce a helper function. If we forget about the list for a minute, we can write a function to convert the string tuple into a simple string:

        let combine(last, first) = first ^ " " ^ last
Given this, it's fairly easy to complete the function:

        let rec convert(lst) =
            if lst = [] then []
            else combine(List.hd(lst))::convert(List.tl(lst))
This technique of introducing a helper function to break down the complexity of the problem into subproblems is a very important technique in OCaml programming. You'll want to use this technique for some of the homework questions.


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