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)
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)
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!
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.