

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:
map
filter
fold (AKA reduce, 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 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.

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
curry
uncurry
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 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 endand 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 endand 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 ... endFor 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 | |