Recitation 4: Curried functions, and list/option polymorphism

Curried Functions

Earlier we said that every function in ML takes exactly one argument, and that we can simulate multiple arguments using tuples. Actually there is one other way to simulate multiple arguments in ML (and other functional languages): we can write our function to take one argument that takes the first argument and returns an (anonymous) function that takes the second argument and returns the result. For example, we could write a function plus that looks like

fun plus (i:int) =
    fn (j:int) => i + j;

Now plus has type int -> int -> int, where the arrow associates to the right. So the expression

- plus 3;
val it = fn : int -> int

returns an anonymous function that takes an integer and adds 3 to it, and the expression

- plus 3 5;
val it = 8 : int

does what we would expect. Thus we can use the function normally (except that we pass the arguments individually instead of packaged into a tuple) or partially evaluated as an anonymous function to be used later.

This device is called currying after the logician H.B. Curry. At this point you may be worried about the efficiency of returning an intermediate function when you're just going to pass all the arguments at once anyway. Run a test if you want (you should know how to find out how to do this), but rest assured that curried functions are entirely normal in functional languages, so there is no speed penalty worth worrying about. In fact, they're so useful and commonplace that ML provides syntactic sugar to make them as easy to define as the functions on tuples that we have been writing. A much simpler way to write our plus function is

fun plus (i:int) (j:int) : int = i + j

This is identical to our earlier definition; we can still call "plus 3 5" to compute 8, or we can call "plus 3" to compute a function that adds 3 to its argument. What are the types of these curried functions? What functions result from partial application of them?

fun lesser (a:real) (b:real) : real = if a<b then a else b
fun equals (x:int) (y:int) : bool = (x=y)
fun pair (x:'a) (y:'b) : ('a * 'b) = (x,y)

Using Polymorphism

The list datatype

Because lists are so useful, ML provides some a builtin parameterized list datatype called list. It acts just like the List datatype that we defined in lecture except that the names of the constructors are changed. The constructor nil makes an empty list (compare to Nil) and the constructor :: builds a new list by prepending a first element to another list (compare to Cons). Thus, list could be declared as:

datatype 'a list = nil | :: of 'a * 'a list

The constructor :: is an infix operator, which is notationally convenient. The SML interpreter knows how to print out lists nicely as well. The empty list is printed as [], and non-empty lists are printed using brackets with comma-separated items. In fact, these forms may be used to write lists as well. Note that nil is a polymorphic value; it is the empty list for all types T list. In fact,  it is given the polymorphic type 'a list. Here are some examples that show how lists work:

- nil;
val it = [] : 'a list
- 2::nil;
val it = [2] : int list
- val both = 1::it;
val both = [1,2] : int list
- case it of x ::
  lst => lst | nil => nil
val it = [2] : int list
- case it of x ::
  lst => lst | nil => nil
val it = [] : int list      (* we don't "recover polymorphism" here; it would be unsafe in general *)
- case it of x ::
  lst => lst | nil => nil
val it = [] : 'a list
- both = 1::2::nil;        (* we can test lists for equality if we can test their elements *)
val it = true : bool
- case both of
=     [x:int, y:int] => x + y (* we can use bracket notation for patterns too. *)
=   | _ => 0;
val it = 3;
- [[]];
val it = [[]] : 'a list list

Just like with datatypes, we have to make sure that we write exhaustive patterns when using case:

- case ["hello", "goodbye"] of (s:string) :: _ => s + " hello";
case ["hello", "goodbye"] ... Warning: match nonexhaustive ...

Built-in lists come with lots of useful predefined Basis Library functions, such as the following and many more:

val null: 'a list -> bool
val length : 'a list -> int
val @ : ('a list * 'a list) -> 'a list		(* append two lists *)
val hd : 'a list -> 'a
val tl : 'a list -> 'a list
val last : 'a list -> 'a
val nth : ('a list * int) -> 'a

Of course, all of these functions could also be easily implemented for the List datatype that we defined ourselves!

We can pattern match on lists much as we could with datatypes. To compute the sum of an int list, we would write

fun sum (l : int list) : int =
    case l of
        [] => 0
      | n::ns => n + sum(ns)

ML provides functions null, to determine whether a list is empty, and length, to compute the length of a list. Unlike our sum function, these functions are polymorphic. How could we write them?

fun null (l : 'a list) : bool =
    case l of
        [] => true
      | _ => false  (* also could have matched _::_ *)

fun length (l : 'a list) : int =
    case l of
        [] => 0
      | x::xs => 1 + length(xs)

How could we write a function that appends two lists?

fun append (l1 : 'a list, l2 : 'a list) : 'a list =
    case l1 of
        [] => l2
      | x::xs => x::(append(xs, l2))

ML provides this function for us in the form of an infix operator @. So we can write

- []@[1,3,5];
val it = [1,3,5] : int list
- [1,3]@[2,4];
val it = [1,3,2,4] : int list

and so on. Remember that the :: operator (cons) takes an element and a list, but the @ operator (append) takes two lists.

ML also provides a function in the basis library map, takes in a function going from 'a -> 'b , and a 'a list , and applies the function to each element in the list, returning the new list. We can write it as such:

fun map (f: 'a -> 'b) (l:'a list) : 'b list =
    case l of
        nil => l
      | x::xs => f(x)::(map f xs)

Multiple type parameters

We saw two related features of SML in class: the ability to produce polymorphic values whose type mentions a type variable and the ability to parameterize types with respect to an arbitrary type variable. As we have seen, polymorphic values are typically function values but other polymorphic values exist, such as nil (and also Nil, as we defined it). Datatypes can actually be parameterized with respect to multiple type parameters; for example the following datatype, or, is a type-level function that accepts a pair of types and yields a new type:

- datatype ('a, 'b) or = Left of 'a | Right of 'b | Both of 'a * 'b;
- Left(2);
val it = (int, 'b) or
- Right("hi");
val it = ('a, string) or
- Both(true, #"a")
val it = (bool, char) or

 Note that the values Left(2) and Right("hi") are still polymorphic with respect to one type!

The option parameterized datatype

Another important standard parameterized datatype is option, which represents the possible presence of a value. It is defined as follows:

datatype 'a option = SOME of 'a | NONE

Options are commonly used when no useful value of a particular type makes sense; this corresponds to some uses of the null value in Java (i.e., NONE acts like null), but there is no danger of encountering a null pointer exception unless an inexhaustive case statement is used or the valOf operation is used. A more detailed description of option is available in the Basis Library documentation.