------------------------

Harvey Mudd College
Computer Science 131
Programming Languages
Fall Semester 1999

Lecture 03

------------------------

------------------------

Last lecture we talked about data. Now let's talk about functions. But wait. We said that in ML functions are first-class data. So we need to be careful about that distinction.

Now what are the things we said you could do with first class data?

But what else can you do. In C, what can you do with an integer that you can't do with a function?

This is a tough one. You may not be able to guess because you are so caught up in the C mindset.

 
 
 
 
 
 
 
 
 
 
 
 
 
 
You can refer to one without giving it a name!

For instance, you can use the literal value 3.141592653 without first assigning it to a variable named, for example, pi.

The notation for a literal function is:


    fn arg_var => body_exp

So, if you want the literal function which adds 1 to an integer, it's:

fn n => n + 1;

The fn says that what follows is a function definition. The n which precedes the arrow is the parameter, and the stuff after the arrow is the returned value.

if you want to apply it to a number, just put it in the function position in an expression:

(fn n => n + 1) 3;

Now, most of the time we want to use functions many times, so we assign them a name. There is nothing special about this, you just use a val like any other naming.

val add1 = fn n => n + 1;

Now, since most functions get named, and this syntax is a little cumbersome, SML provides a simpler declaration form for functions:

fun add1 n = n+1;

This other form of declaration is not just syntactic sugar for the first form. There is an important difference in functionality (no pun intended). Consider this declaration:

val badfact = 
      fn n => if (n <= 1) 
                then 1 
                else n * (badfact (n-1));

It doesn't work because at the time the expression on the right is being evaluated for so that it can be given the name badfact the name badfact doesn't yet exist.

In contrast, a name declared using fun is available in the expression the name is being assigned to:

fun fact n = if (n <= 1) 
               then 1 
               else n * (fact (n-1));

Now it is actually possible to do recursion with unnamed functions (believe it or not you can do it without ever assigning them a name) but we'll talk about that later when we talk about the lambda-calculus.

------------------------

This works for defining recursive functions. But what if you want to write a pair of mutually recursive functions? Suppose, for example, that you want to write the following non-sensical functions:

f(0) = 0
f(n) = g(2n)

g(1) = 1
g(n) = f(n div 3)

There is no way with what we have so far that you can define them, since f needs the definition of g and vice versa. We are apparently stuck since one function or the other must be defined first.

Therefore, SML provides and to allow mutually recursive definitions. You can tie any two (or more) definitions of the same sort (val, fun, datatype, etc.) together by excluding the sort indication from the later ones and replacing it with and. So, we would define these two functions as:

fun f 0 = 0
  | f n = g (2*n) 

and g 1 = 1 
  | g n = f (n div 3) 
Note that when you use and you can't put a semicolon after the first definition, since ML uses that as the end of a definition and these must be defined together. In general, it is not usually necessary to use semicolons at all in your source files. In most cases (i.e. when a file just contains function and type definitions and not expressions) the syntax is unambiguous enough for SML to figure out where one definition ends and another begins. The only difference is that if you use semicolons then compilation (and type inference) will be forced each time it encounters a semicolon. Without semicolons it will wait till the end of the file to begin compiling and infering types.

In general, mutual recursion should be avoided whenever possible as it is hard to read and comprehend. But sometimes it is the best solution.

------------------------

Now, the definition of factorial above uses an if-then-else to test its input argument, in a manner pretty familiar to most of you. For the fibonacci function (here's a gift) you could do the same thing twice:

fun fib n = 
      if (n = 0) 
        then 0 
        else if (n = 1) 
               then 1
               else fib (n-1) + fib (n-2);

Or you could use the SML case statement:

fun fib2 n =
       case n of
            0 => 0
          | 1 => 1
          | _ => fib2 (n-1) + fib2 (n-2);

Here each rule of the case is checked one at a time and the first one whose head matches is selected. The _ in the last case will match anything. Now, as with if-then-else statements, a case must have a value (of the same type) for every possible call. So, the compiler is very careful to check the range of the type against the matches you provide. Look at these two functions:

fun booltest b = case b of
                    false => "liar"
                  | true => "saint";

fun badinttest i = case i of 0 => "tiny" | 1 => "small";

The compiler will accept the latter function on the premise that maybe you know that you will only ever use it on integers that are guaranteed to be 0 or 1. But it warns you that there is a potential danger of a run-time error creeping into your program.

The best policy is to make sure you always have a case for all possible matches. Cases you don't expect to have arise should generate some internal error using the exception mechanism. But we haven't talked about that yet, so here we'll just return an error message:

fun inttest i = case i of
                   0 => "tiny"
                 | 1 => "small"
                 | _ => "out of range";

------------------------

Now the designers of SML realized that an enormous amount of code in most languages is written just to select between different cases of the input arguments. So, they included syntactic sugar that weaves the case statement directly into the fun declaration. The way you would really write the fibonacci function is:

fun fib3 0 = 0
  | fib3 1 = 1
  | fib3 n = fib3 (n-1) + fib3 (n-2);

------------------------

Important Note:

It is crucial to make sure that you type the names of constants and constructors correctly. If you mispell the constructor and use a valid variable identifier, the pattern will match any input. For example:

fun booltest2 fase = "liar"
  | booltest2 true = "saint";

fun inttest2 O = "tiny" % Thats an "Oh" not a "Zero" | inttest2 1 = "small" | inttest2 _ = "out of range";

------------------------

Pattern matching is a very powerful tool in ML for shrinking the size of code. Suppose we want to write a function that will take a pair of integers and return a pair with each position incremented. We could write:

fun pairinc1 pr = 
       ((#1 pr) + 1, (#2 pr) + 1);

But, as I said earlier, the destructors for the structured types are rarely used, because of the pattern matching facility. The proper way to write this function is:

fun pairinc2 (a,b) = (a+1,b+1);

Now, if we want a version that will leave pairs that have a zero in either position alone, we just write:

fun pairinc3 (0,a) = (0,a)
  | pairinc3 (a,0) = (a,0)
  | pairinc3 (a,b) = (a+1,b+1);

This is a little awkward though, because we have to rebuild the pair we took as input as the output in the first two cases. For this reason SML provides what are called layered patterns:

fun pairinc4 (pr as (0,a)) = pr
  | pairinc4 (pr as (a,0)) = pr
  | pairinc4 (a,b) = (a+1,b+1);

Here, if the first case matches, the name a is assigned to the value in the second slot, but the name pr is assigned to the whole pair. If the second case matches, the name a is assigned to the value in the first slot, and the name pr is assigned to the whole pair.

Now, as it happens, we don't care what the non-zero element of the pair is anymore (since pr is the whole pair) so the best way to write this would be:

fun pairinc5 (pr as (0,_)) = pr
  | pairinc5 (pr as (_,0)) = pr
  | pairinc5 (a,b) = (a+1,b+1);

------------------------

Pattern matching isn't actually specific to function definitions. It can also be used in val declarations, as in:

val (a,b) = pairinc5 (2,3);

or

val pr as (c,d) = pairinc5 (4,10);

------------------------

Now, so far all our functions have taken just a single parameter. Things get more interesting when we start looking at functions of more than one parameter.

Suppose we want to write a function that just adds two numbers together. We could give the two numbers to the function as a tuple, like:

fun add (x,y) = x + y;
But we can also give them as two separate parameters as in:
fun add' x y = x + y;
Now, the type of the first function is easy to understand:
val add = fn : int * int -> int
But, what does the second version's type mean?
val add' = fn : int -> int -> int

We said that when we call the function, as in:

add' 3 4;
it's as though we had parenthesised it as:
(add' 3) 4;
This seems to indicate that when you apply add' to 3 it returns a function which then gets applied to the 4. In fact that's just what happens, and just what the type said. In types, the arrow associates to the right, which means that the type reported before is equivalent to:
val add' = fn : int -> (int -> int)
Which says that add' takes an integer as a parameter and returns a function which takes an integer and returns an integer.

In other words when we apply add' to 3 it returns a function which, when it gets an integer returns the result of adding 3 to that integer.

This is really what's happening. I can prove it by just storing the result of applying add' to 3.

val add3 = add' 3;
add3 4;

------------------------

This is a very powerful idea.

First of all, from a formal standpoint it means that when we are in a language where functions are first class data types we only ever need to consider functions of a single parameter.

Second, as we'll se in some examples over the next few weeks it is often very useful to store the result of such a partial application of a function.

I should note that the fact that we can restrict our view to functions of one argument is emphasized by the fact that the fn form for writing unnamed functions only allows a single parameter. So, in that style we would define add as:

val add'' = fn x =>
              fn y => x + y;

Sometimes, when we want to emphasize that a function will mostly be used to return another function we will mix the two styles. So, for instance, if we mostly intended to use the add function to build custom adder functions for given integers, we might right it like this:

fun add''' x = fn y => x + y;

It's important to realize that all three ways of declaring the add function are equivalent. You should use whatever style is most indicative of your intended usage. Most of the time that will be the regular fun style used for add'.

By the way, the formal name for having a function of two parameters implemented instead as a higher-order function that takes a parameter and returns a function which takes the second parameter is called currying the function. It's named for Haskell Curry who was one of the inventors of the lambda-calculus which is the formal system functional languages are built on.

------------------------

Now, you may have noticed that I mostly don't give the types of my function parameters. In some books they do give the types for clarity.

Whether you chose to give the types is up to you. If you don't the system will infer the types for you and give you the most general types that the function can be applied to.

In real ML programming, in which functions are gathered together in modules, it is most common to not give the types at the functions, but to supply them in the description of each function which is put in the module header. This is pretty much like supplying prototypes in C and C++.

------------------------

What do I mean by "the most general type possible"? Well, so far, most of the functions we have written have been math oriented, and work on integers or reals. Here is a more interesting function that works on lists:

fun addUp nil = 0
  | addUp (h::t) = h + (addUp t);
Notice it's type: fn : int list -> int. Why is that?

Now, consider the following function:

fun length nil = 0
  | length (h::t) = 1 + (length t);
In this function, though the recursive structure is identical to addUp, there is an important difference: we are never actually doing anything with the individual elements of the list other than noting that they are there. This means we don't really care what kind of thing is in the list. ML tells us this by saying that the type of this function is fn:'a list -> int. When ML wants to tell us that a function can take any type it uses greek letters as variables for the types; 'a is the way ML writes alpha in ascii.

------------------------

ML will always report the most general type it can for a function. In length the only constraint on the parameter is that it must be some kind of list. We can get even more general than that. For instance:

fun id x = x;
puts absolutely no constraints on its parameter. All it does is take it in and return it back. These operations can be applied to any first class type. So, the type of the function is just fn : 'a -> 'a.

Finally, consider the function:

fun K x y = x;
This takes two parameters tosses out the second and returns the first. Now we could say the type is fn : 'a -> 'a -> 'a, but that constrains the two parameters to be of the same type. There is no such constraint forced by the program. So, the type is reported as fn : 'a -> 'b -> 'a.

------------------------

An interesting class of constraints is seen in the following two programs:

fun foo e lst = if length (e::lst) > 3 
                  then true 
                  else false;
The type of this function expresses the fact that we don't care what kinds of list and element we apply it to, as long as the element type matches the list type. Now, consider the member function:
fun memb e nil = false
  | memb e (h::t) = if h = e 
                      then true 
                      else memb e t;
Seemingly here all we care about is the same thing, that the element and list types match. But that's not quite true. Here we are manipulating the element just a little bit. We are checking it against the elements of the list for equality. Now, not all first class types can have this done. In particular you are not allowed to ask if two functions are equal (Why?). So, ML tells us that this will work on almost any first class type except that it has to be one of the types that has an equality check. That's what the extra apostrophe in the type means. Types that support equality checking are called equality types.

------------------------

One additional use of and that isn't often discussed is that it allows two definitions that are not necessarily mutually recursive to share type information. For example, consider the following silly pair of definitions:

fun f m n = g (m * n) 

and g n = n / 2;

Ordinarilly, the use of the multiplication operator, if not explicitely noted or inferable otherwise, is assumed to be an integer multiplication. Here, though the mutual definition keyword and allows the definition of the first function to draw type information from the second definition.

Obviously in this case we could just reverse the order of the definitions, but there may be times when we have a strong preference to have some definitions in a particular order.

------------------------

This page copyright ©1999 by Joshua S. Hodas. It was built on a Macintosh. Last rebuilt on Tuesday, August 31, 1999 at 8:39:10 PM.
http://cs.hmc.edu/~hodas/courses/cs131/lectures/lecture03.html