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

Harvey Mudd College
Computer Science 131
Programming Languages
Spring Semester 2000

Lecture 04

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

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

What is a higher-order function?

It is just a function that takes or returns another function.

A first-order function is one which operates on individuals (anything that is not a function). A second-order function operates on first-order functions, and so on. A higher-order function means anything that is not first-order, or, more generally, a function that will operate on functions of any order.

Curried functions are higher-order in the sense that they return functions as a result of their partial application. Here are a couple of examples of functions that take functions as parameters.

fun double f a = f (f a);

fun quadruple f a = (f (f (f (f a))));

fun quadruple' f a = (double (double f) a);

fun quadruple'' f   = double (double f);

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

The first three H.O. functions I want to talk about to manipulate lists, the most popular data structure in functional programming. The functions are:

I say they are higher order because they are each designed to take a function as one of their parameters.

I say they are useful for reasons you will come to appreciate over time. But, I'll give you an example of how useful they are. In 1979 a researcher named Richard Waters did a study of the Fortran Scientific Subroutine Library which was a major package of many thousands of lines of code in common use at the time. He determined that if fortran were capable of supporting these three functions, roughly 60 percent of the code in the library could be rewritten as calls to them. This would have significantly reduced the size of the library and also means that optimizations in the code produced for just these functions would have widespread benefit.

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

Let's look at map first, because it is the easiest to understand. The idea behind map is that it takes a function (of type 'a -> 'b) and a list of items (of type 'a) and returns the list (of elements of type 'b) that results from replacing each of its elements by the result of applying the function to that element.

Now, map is built in, but it is simple and instructive to write ourselves.

fun map f nil = nil
  | map f (h::t) = (f h)::(map f t);

So, suppose we want to add one to every element of a list, we could use map as follows:

map (fn x => x+1) [3,7,9];
If we want a function that will add one to every element of a given list we just write:
fun incList l = map (fn x => x+1) l;
or, more succinctly, using partial application:
val incList = map (fn x => x+1);

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

The function filter takes a predicate, which is to say a function from 'a to bool and a list of elements of type 'a and returns the list of those elements for which the predicate returned true. Filter is written as:

fun filter p nil = nil
  | filter p (h::t) = 
       if p h
         then h::(filter p t)
         else filter p t;
So, if we want to remove all the 0' from a list we would write:
filter (fn x => x <> 0) [2,0,3,4,0,5,6,7,0,9];
and, as above, we could store this capability off in a function as in:
val remove_zeroes = filter (fn x => x <> 0);
Similarly, if we wanted to write a remove_odd function which removes the odd numbers from a list of integers, we could do it by first defining a predicate that tests whether a number is odd:
fun odd n = (n mod 2) = 1;
and then using that in a call to filter, as in:
fun remove_odd l = filter (fn n => not (odd n)) l;
Obviously we could also do this a bit more simply by defining an even predicate instead of an odd predicate.

The remove_vowels function from the first assignment can be coded as a call to filter. Think about how you would do it.

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

The last of these predicates, fold is the most complicated. In fold the idea is to successively combine all the elements of the list together to get a single answer. For instance, you could use fold to add up the elements of a list. Let's look at how you would use it, then we should be able to figure out how you'd write it.

fun sumList l = fold (op +) 0 l;
So, the first parameter is what you want to do to the successive values of the list; the second parameter is the value you want to use to get the ball rolling, and the third value is the list. To write it we say:
fun fold f base nil = base
  | fold f base (h::t) = 
       f (h,fold f base t);
To use it to multiply the elements of a list, you would say:
val multList = fold (op * ) 1;
Now, this version of fold assumes that the function its given will be a function on pairs. It would be easy enough to change it so that it took a curried function of two separate arguments instead.

The function fold is built in but is named foldr. (The function foldl is also built in. It is similar to fold, but associates to the left.)

Both of these functions are defined in the structure List, but are exported at the top level, so you don't have to use their fully-qualified names. The same is true of map. The function filter is also defined in List, but is not exported at the top level, so you must refer to it as List.filter.

------------------------
Just to show some of the other things you can do, consider that you could write map as a partial application of fold. As in:
fun map f l = fold (fn (h,newt) 
                            => (f h)::newt)
                   nil l;

There are many other examples of things you can do with these three functions. I'll challenge you to some of them on the next assignment. As you work on the assignment and try to use fold to implement other functions, you may find it useful to start some problems from a function that really just implements the identity function on lists, but in a slow and complicated way:

fun id l = fold (fn (h,newt) => h::newt) nil l;

Then think of how what you want to do to the list differs from the identity function, and where you need to inject changes into this skeletal function.

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

As you start to program using higher-order techniques you quickly find that three more simple higher-order functions can help quite a bit. Those functions are

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

The first function curry comes in handy when you have a function of two parameters that does what you want, but it expects its two parameters as a pair, and you want to give it the parameters one at a time.

For instance, suppose we want to write a function add that adds two integers. We could just write it manually, as in:

fun add x y = x + y;
But this is one case of a more general process. We can define curry as a function that will work on any pair-based function:
fun curry f =  fn x => fn y => f (x,y);

Now with curry we can just say:

val add = curry (op + : int * int -> int);
In other words, we are just assigning the name add to the curried version of the integer plus operator.

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

Similarly, uncurry takes a function that expects its parameters one after the other and gives you a function that does the same thing only it takes the parameters as a pair:

fun uncurry f = fn (x,y) => f x y;
Now, we can define:
val plus = uncurry add;

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

The function compose takes two functions and returns another function that is equivalent to the two of them combined. For example, suppose you have a function squares that will square all the items in a list, defined as:

val squares = map (fn x => x * x);
and another function sum that adds up the elements of a list, as in:
val sum = fold (op +) 0;
Now, if we want to define a function sumSquares you can't just say
val sumSquares = sum squares;
because it's ill-formed in terms of the types (why?). So, we need to write:
fun sumSquares l = sum (squares l);
With compose, however, we can just write:
val sumSquares = compose sum squares; 
That is, in general:
(compose f g) x == f (g x)
Compose itself is just defined as:
fun compose f g = fn x => f (g x);

You should make sure you understand the types of all these functions.

While curry and uncurry are not built in, compose is built in as the infix operator o. So you would really write:

val sumSquares = sum o squares;

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

The next thing I want to talk about is the question of how names are scoped in ML. At present we have only seen two kinds of names: global ones and local formal parameter names.

You may not have experimented explicitely with the use of global names in your functions yet, and that's a good thing, because they are always a potential source of confusion, but you should understand that they are there.

For instance, we could define the following names:

val x = 3;
fun addx y = x + y;
addx 5;
Now, what happens if we "redefine" x?
val x = 7;
addx 5;
This seem wierd, but it is a consequence of the meaning of names in ML. Unlike in imperative languages in which variables are the names of storage positions, in ML and other functional languages, variables are just names (or shorthand) for expressions. The easiest way to think about it is that when we use a val statement we are not assigning a value to a name but a name to a value.

When I assigned the name x to 3 and then used it in addx I should have been thinking of x just as a shorthand for 3. If that's so, just because later I decided to use x as shorthand for some other value, doesn't mean that I didn't want it to mean three at the time I wrote addx.

Obviously, though, addx was a lousy name; I should have called it add3;

While this may seem strange at first, it is actually a much more consistent behavior than in imperative languages. After all, if you write:

x = 3;
y = x + 2;
x = 4;
cout << y;
in C++, you don't expect the system to print 6 do you?

Notice that this rule applies to all names. If I define a function, then another function which uses that function, and then I decide to redefine the original function, the second function will still continue using the original definition of the first function, even though the new definition is the one available at the top level. Watch:

fun add1 x = x + 1;
fun add2 x = add1 (add1 x);
add1 4;
add2 4;
fun add1 x = x + 3;
add1 4;
add2 4;
This may have confused you at times during your programming/debugging sessions.

Now, we'll talk next class about what to do if we really really really want to be able to change the value associated with an existing name such that it permeates to things already defined in terms of that name. But for now I just wanted to get the ball rolling in our discussion of scope.

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

Often we want to define a name so that it is local to some scope. This can be useful either if we want to just have a name that is used to store some temporary result, or if we want some function that is used to support the definition of another function but that shouldn't itself be externally available.

There are two main ways to restrict the scope of a name in ML: The let construct, and the local construct. The syntax for them is as follows: The let construct is written:

let
   definition1
   definition2
   ...
in 
   expression
end
and it is used in any situation that you would normally want an expression. That is, the value of the whole construct is the value of the expression after the in.

The local construct is written:

local
   definition1
   definition2
   ...
in 
   definition
end
and it is used anywhere you are making a definiton, rather than computing a value.

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

So, for instance, the local function is most often used to restrict access to some auxillary function. For example:

local
   fun factAux result i n =  
          if (i > n)
            then result
            else factAux 
                    (i * result)
                    (i+1) n;
in
   fun fact n = factAux 1 1 n;
end;

This way the user has access to fact but has no idea factAux exists. Notice the compiler keeps its mouth shut concerning factAux.

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

The let construct can be used either to provide temporary names, or to hide a function definition. For example, before we defined filter as:

fun filter p nil = nil
  | filter p (h::t) = 
       if p h
         then h::(filter p t)
         else filter p t;
We could save a little typing, and perhaps make things clearer by writing:
fun filter p nil = nil
  | filter p (h::t) = 
      let
         val filter_t = filter p t;
      in 
         if p h
           then h::filter_t
           else filter_t
      end;
In this case there is no difference in efficiency since the expression is only ever used in one of the two places. But if it were actually computed in more than one place we would be saving computation as well as typing.

As an example of hiding a function, we can use let in much the same way as local:

fun fact n = 
      let
         fun factAux result i n =  
                if (i > n)
                  then result
                  else factAux 
                         (i * result)
                         (i+1) n
      in
         factAux 1 1 n
      end;

One of the nice things about using let for defining a sub-function, is that since it is under the scope of the main function definition, the parameters of that function are global to it. So, in the last example we don't need to pass n to factAux since it never changes during a run of that function. Instead we can write the definition as:

fun fact n = 
      let
         fun factAux result i =  
                if (i > n)
                  then result
                  else factAux 
                         (i * result)
                         (i+1)
      in
         factAux 1 1
      end;

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

While the let and local constructs enable us to restrict the scope of names in one way, another issue in programming-in-the-large is being able to divide up the global name space so that names used in different global contexts do not conflict. This is the purpose of adding a module system to a language.

The SML module system provides a clean mechanism for gathering related pieces of code together and for controlling which parts of the code are accessible from the outside. At the same time, all the internal code can be easily tested at the top-level with little or no extra work required for final packaging.

The SML module system is built on three pieces: structures, signatures, and functors. Here we will describe just the most basic part of the system, structures, which correspond to the basic notion of module provided in most other languages.

A structure is simply a collection of type and value definitions gathered together so that they may be loaded together, and accessed through one name.

A structure declaration has the form:

structure structureName =
struct
   definition1
   definition2
   ...
end
For example, we might gather together the last three useful higher-order functions into a single structure name ho
structure ho =
struct   
   fun compose f g = fn x => f (g x)
   
   fun curry f = fn x => fn y => f (x,y)
   
   fun uncurry f = fn (x,y) => f x y   
end

When the structure is loaded into SML, the system echos back the name of the structure, and the names of all the defined objects and their types.

These values are not available at the top-level. Rather, the items defined in the structure must be accessed through their fully qualified names. which are formed by prepending the name of the structure and a dot, as in ho.curry.

------------------------
This page copyright ©2000 by Joshua S. Hodas. It was built on a Macintosh. Last rebuilt on Monday, January 31, 2000.
http://www.cs.hmc.edu/~hodas/courses/cs131/lectures/lecture04.html