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