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

Harvey Mudd College
Computer Science 131
Programming Languages
Fall Semester 1999

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 functions I want to talk about are all designed to manipulate lists, which are the most popular data structure in functional programming. The functions are called map, filter, and reduce. The function reduce is also sometimes called fold or accumulate.

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 inc_list l = map (fn x => x+1) l;
or, more succinctly, using partial application:
val inc_list = 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, reduce is the most complicated. In reduce the idea is to successively combine all the elements of the list together to get a single answer. For instance, you could use reduce 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 sum_list l = reduce (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 reduce f base nil = base
  | reduce f base (h::t) = 
       f (h,reduce f base t);
To use it to multiply the elements of a list, you would say:
val mult_list = reduce (op * ) 1;
Now, this version of reduce 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 map is built in to SML. The function reduce is built in but is named foldr. (The function foldl is also built in. It is similar to reduce, 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 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 reduce. As in:

fun map f l = reduce (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 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 curry, uncurry, and compose.

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

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:int) 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:int) => x * x);
and another function sum that adds up the elements of a list, as in:
val sum = reduce (op +) 0;
Now, if we want to define a function sum_squares you can't just say
val sum_squares = sum squares;
because it's ill-formed in terms of the types (why?). So, we need to write:
fun sum_squares l = sum (squares l);
With compose, however, we can just write:
val sum_squares = compose sum squares; 
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 sum_squares = sum o squares;

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

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