CSE341 Notes for Wednesday, 04/01/09

I began by discussing the benefits of having a clear and concise understanding of a programming language. I plan to incorporate several lecture slides that come from Dan Grossman's 341 course into future lectures. I plan to use them to discuss important concepts in a more precise way.

We then explored how to define functions in ML and I demonstrated the more usual interaction with the ML interpreter where we edit a file of ML definitions in one window and in the other window we keep restarting ML and loading in the new version of the definitions.

First I discussed some basic functions using type notations for the function arguments:

        fun inc(n : int) = n + 1;
        fun sqr(n : real) = n * n;
I pointed out that ML 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 above doesn't need the type specification because of the constant 1:

        fun inc(n) = n + 1;
When we changed the 1 to 1.0, we found that ML correctly identified this as a function that takes a real and returns a real. I tend not to include type specifications unless I need to. But what happens when there isn't a value like 1 or 1.0 to make the type clear, so I typed in this function:

        fun sqr(n) = n * n;
The issue is that multiplication is overloaded with integer multiplication and real multiplication. Some people thought this would produce an error and that's what it did in the older versions of ML. ML 97 instead defaults to type int. So if we wanted a function for squaring reals, we'd have to include a type specification. One interesting aspect of ML is that you can specify that type in many different ways. For example, you can say that the parameter is of type real anywhere the parameter is mentioned:

        fun sqr(n:real) = n * n;
        fun sqr(n) = (n:real) * n;
        fun sqr(n) = n * (n:real);
Or you can say that the function result is real:

        fun sqr(n) = ((n * n) : real);
At this point I switched to using two windows. In one window I ran the emacs editor to edit a file of definitions. In the other window I ran the sml environment to load and execute the definitions. This is the way most people use ML. You can use any editor you like for creating the files. We saw that you can load the definitions in the interpreter by giving a command like this:

        use("wed.sml");
I mentioned that it's in some ways easier to exit the interpreter every time and execute a command like this at the command prompt:

        sml wed.sml
This is particularly easy when you're working with the same file repeatedly. You have to type the command above the first time you want to use the file, but then you can get by with three keystrokes. Type ctrl-d to exit ML. Then type the up-arrow key to get back this command. Then type enter to execute it. An added benefit is that it starts you with a fresh version of the ML environment each time.

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.

For our recursive definitions, we'll need to use the if/else construct, which has the following general form:

if <boolean expression> then <expression> else <expression> The first example we looked at is a factorial function:

        fun factorial(n) =
            if n = 0 then 1
            else n * factorial(n - 1)
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 *)
        fun factorial(n) =
            if n = 0 then 1
            else n * factorial(n - 1)
I said that anyone who is interested is allowed to define an exception, which is really the right way to handle this kind of situation:

        exception undefined_factorial;
        (* pre: n >= 0 *)
        fun factorial(n) =
            if n < 0 then raise undefined_factorial
            else if n = 0 then 1
            else n * factorial(n - 1)
Then we looked at some examples of list recursion. We wrote this code to find the last element of a list:

        fun last(lst) =
            if length(lst) = 1 then hd(lst)
            else last(tl(lst))
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 first name followed by last name, as in:

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

        ["Clinton, Hillary", "Obama, Barack", "Biden, Joseph"]
We started a recursive definition:

        fun convert(lst) =
            if lst = [] then []
            else ...
In my code, I tend to test whether a list is equal to "[]" (the empty list). Ullman tends to compare against "nil". They are the same thing, so you can use whichever test makes more sense to you.

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:

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

        fun convert(lst) =
            if lst = [] then []
            else combine(hd(lst))::convert(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 ML programming. You'll want to use this technique for some of the homework questions.


Stuart Reges
Last modified: Wed Apr 1 23:05:19 PDT 2009