Power
Acknowledgement: This note created by Pramod Subramanyan and David Walker.
Polymorphism and Higher-Order Programming
Good programmers are lazy: they never writes the same piece of code twice. Instead they strive to factor out the common bits in to meaningful, reusable components. Write a component once, find and fix all bugs, and use it many times. That is the path to becoming efficient programmer. If you need to update the component, perhaps for performance or to fix an error, it only needs to be updated in one place.
In OCaml, higher-order and polymorphic functions are powerful tools for code reuse. Higher-order functions are those functions that take other functions as arguments or return functions as results, while polymorphic functions are functions that act over values with many different types. Together, they enable a great deal of code reuse. In this lecture, we will look specifically at how to use higher-order and polymorphic functions to represent complex, recursive, control-flow patterns.
Higher-Order Programming
Consider the following two functions which (a) increments all the elements in a list and (b) squares all the elements in a list.
let rec inc_all (xs:int list) : int list = match xs with | [] -> [] | hd::tl -> (hd+1)::(inc_all tl) let rec square_all (xs:int list) : int list = match xs with | [] -> [] | hd::tl -> (hd*hd)::(square_all tl)
The only difference between inc_all
and square_all
is in the
expressions hd+1
and hd*hd
--- the other parts of these
functions are exactly the same. OCaml's higher-order functions make it
easy to extract these commonalities out in to a reuseable component.
Below, we present the map
function, which applies its
argument f
to all elements of a list.
let rec map (f:int->int) (xs:int list) : int list = match xs with | [] -> [] | hd::tl -> (f hd)::(map f tl)
map
is one of the most ubiquitous OCaml functions --- you
should get used to reading and writing programs that use it. With
map, recursive functions like inc_all
and square_all
become simple, non-recursive one-liners
as shown below.
let inc x = x+1 let inc_all xs = map inc xs let square y = y*y let square_all xs = map square xs
Anonymous Functions
When programming with higher-order functions like
map
, one has a tendency to need many little functions
like inc
and square
, which are then often only used
once. Rather than defining a named
function to be used just once, we can define it without a name
and use it in place. For instance we would usually write
inc_all
and square_all
as follows.
let inc_all xs = map (fun x -> x + 1) xs let square_all xs = map (fun y -> y * y) xs
The expression fun x -> x + 1
is an
anonymous function that takes one argument (x
) as
input and returns x+1
as a result. One can also define
multi-argument functions using the syntax
fun x y z -> x + y * z. However, one cannot define recursive functions --- one must have a function name for that.
Conceptually, anonymous functions are no more complicated than anonymous
numbers (like 3 or 4), anonymous strings ("hello"
)
or any other anonymous value. It would certainly be annoying if instead
of writing:
print_string ("hello" ^ " " ^ "world")one had to explicitly bind names to each of the strings first:
let hello = "hello" in let space = " " in let world = "world" in print_string (hello ^ space ^ world)
Why should function values be treated differently from other values like integers or strings? They shouldn't!
Non-anonymous (Conspicuous?) Functions as Anonymous Functions
It turns out that the function definitions we have been using so far are actually abbreviations. The following code:
let square x = x*x let add x y = x+yis just syntactic sugar for:
let square = (fun x -> x*x) let add = (fun x y -> x+y)
With this in mind, it is easy to see that several of the functions we have written earlier are equivalent:
let square x = x*x in map square xs == let square = fun x -> x*x in map square xs == map (fun x -> x*x) xsThe 3rd expression is derived from the second by substituting
fun x -> x*x
for the variable square
.
A comment on style: One must be somewhat careful with
anonymous functions. They are great when one needs to define a small
function (like square
or increment
)
that is used once. However, if one must define a larger function,
it is typically better to give it a name, because it will be easier
for a colleague or teammate to read. Use your judgement and
for more tips, see our style guide.
Polymorphic Functions
map
seems like a pretty great function until we stumble across div_all
:
let rec div_all (xs:float list) : float list = match xs with | [] -> [] | hd::tl -> (hd /. 2.0)::(div_all tl)Once, again, the code of
div_all
is almost identical to the code of
square_all
or inc_all
and it seems like we should be
able to implement this using map
, but we can't. map
operates over integer lists whereas we need a function that operates over floating point
lists. Fortunately, we can redefine map to make it more general. There is no reason
to constrain map
to operate over integers alone, we can define it to
work over lists with elements of any type 'a
and transform them in to lists of any other
type 'b
.
let rec map (f:'a -> 'b) (xs:'a list) : 'b list = match xs with | [] -> [] | hd::tl -> (f hd)::(map f tl)
In general, in OCaml, whenever a type name is preceded by an apostrophe (as in
'a
and 'b
), it is a type variable that may stand for
any type. If one were to write out the full type of map, it would be the following:
map : ('a -> 'b) -> 'a list -> 'b listWe would read the type as saying "for all types
'a
and 'b
,
map takes a first argument with type 'a -> 'b
, a second argument with
type 'a list
and produces a result with type 'a list
."
To understand how we might use a polymorphic value like map
, we can
substitute any concrete type we like for the type variables that appear in map
's
type. For instance, if we substitute int
for 'a
and
bool
for 'b
in the type of map, we wind up with a type
like this:
(int -> bool) -> int list -> bool listConsequently, we could use map at that type in the following expression:
let pos : int -> bool = fun n -> n > 0 in map pos [1;2;3;-1;-2;-3]
Alternatively, if we substitute float
for 'a
and
float
for 'b
in the type of map, we wind up with a type
like this:
(float -> float) -> float list -> float listWe can use map at that type to implement
div_all
:
map (fun x -> x /. 2.0) [5.0; 7.0; -3.2]
Finally, there is nothing stopping us from substituting arbitrarily complex types like
list types and option types and tuple types and other function types for the type variables
'a
and 'b
. For instance, below, we substitute the type
int list
for 'a
and also for 'b
.
map (map (fun x -> x + 1)) [[2]; [4;5]]
A Generic Reducer
The higher-order function map
implements one very common
recursion pattern over lists, but there are more.
Consider the following two functions. What do they have in common?
let rec sum (xs:float list) : float = match xs with | [] -> 0.0 | hd::tl -> hd +. (sum tl) let rec all_pos (xs:int list) : bool = match xs with | [] -> true | hd::tl -> (hd > 0) && (prod tl)
Both functions are defined using two cases -- one base case for the empty list, and one
recursive case for a non-empty list. The base case returns a specific, pre-determined value.
The recursive case makes a recursive call over the tail of the list and uses the result
of that recursive call, together with the head of the list in a computation that produces
the final result for that case. To capture this recursion pattern, we will define
a function reduce
that has the following property.
reduce f u [x1; x2; x3; ...; xn] == f x1 (f x2 (... (f xn u)))For instance:
reduce (+.) 0.0 [1.0; 2.0; 3.0] == 1.0 +. (2.0 +. (3.0 + 0.0))or
let pos (x:int) (b:bool) : bool = (x > 0) && b) reduce pos true [1; 2; 3] == pos 1 (pos 2 (pos 3 true))Here is our definition.
let rec reduce (f:'a -> 'b -> 'b) (u:'b) (xs:'a list) : 'b = match xs with | [] -> u | hd::tl -> f hd (reduce f u tl)
A Note on MapReduce
It is worth noting that the functions very similar to the map and reduce we've defined above are the basis of Google's MapReduce framework. If you're interested in learning more, this paper from OSDI 2004 and a related paper from HPCA 2007 are good places to start reading.
Curried Functions and Partial Application
It turns out that all functions in OCaml are unary (1-argument) functions! But how can this be? Didn't we just write functions with 2 and 3 three arguments while writing map and reduce?
The following declaration of a function that seemingly accepts two arguments:
let add = (fun x y -> x+y)
is actually shorthand for the following.
let add = (fun x -> (fun y -> x+y))
Let's parse the complicated definition of add
. We will start from the
inside and work our way out. The innermost expression fun y -> x + y
declares a function that takes as argument an integer y
and returns
the integer x+y
.
Where does x
come from? We see that x
is bound from the
definition of outer function (fun x -> (fun y -> x+y))
. So the way to
understand this is that this expression creates a function that takes a single
argument x
and returns the function fun y -> x + y
. In
other words, add is itself a single argument function and when applied on an
argument x, it turns returns another single argument function that adds x to
the argument supplied (y) to the latter function.
This is a slightly subtle concept, so let's look at an example OCaml session that might help explain it.
# let add = (fun x -> fun y-> x + y);; val add : int -> int -> int =# add;; - : int -> int -> int =
The session shows that add has type int -> int -> int which we now realize means that it is of type int -> (int -> int), or equivalently add takes an argument of type integer and returns a function of type integer to integer.
# let add2 = add 2;; val add2 : int -> int =
We've defined add2 by applying the argument 2 to the function add. As we'd expect, the type of add2 is a function from integer to integer.
And what does add2 do?
# add2 3;; - : int = 5 # add2 10;; - : int = 12 # add2 100;; - : int = 102
It simply adds the integer 2 to its argument.
Partial Application
This process of applying fewer than n arguments to a n-argument function is called partial application. The function add2 was defined by partially applying the argument 2 to the function add.
Another example of partial application is the following:
let inc = add 1