Essentially, the idea is the following: consider a function f: 'a * 'b -> 'c. This function takes a pair of arguments, the first of type 'a, and the second of type 'b, and gives back a result in 'c. There is only one way f can be applied: by passing the two arguments together. Given x:'a, and y:'b, we write f(x,y) for the application of f to the pair(x,y).
We could imagine now a variant of f that, instead of taking the arguments together, takes them "one at the time", and gives the same result as f when it is supplied with both arguments. This variant is called curried version of f. Let us denote it by f_{c}. The type of this function is f_{c}: 'a -> 'b -> 'c, and, by definition, we have that for every x:'a, and y:'b, f(x,y) = f_{c} x y holds. The main difference between f and f_{c} is that the latter can be applied also to the first argument only: The expression f_{c} x (for x:'a) is perfectly legal and denotes a function of type 'b -> 'c (the function which, when provided with an input y:'b, will give as result f(x,y)).
In order to provide support for curried functions, a language needs to be Higher Order. The currying adds flexibility and expressivity to the language, and it is part of the general principle of abstraction. In a sense, currying is intrinsic to the philosophy and design of an higher-order language. Below we will see some examples of how this feature adds expressivity.
Let us see how the currying possibility can be used in ML.
- fun append([],k) = k | append(x::l,k) = x::append(l,k); val append = fn : 'a list * 'a list -> 'a listThe curried version of append can be defined in the following way:
- fun append_c [] k = k | append_c (x::l) k = x::(append_c l k); val append_c = fn : 'a list -> 'a list -> 'a listor, equivalently:
- fun append_c [] = (fn k => k) | append_c (x::l) = fn k => x::(append_c l k); val append_c = fn : 'a list -> 'a list -> 'a listWe can now write expressions like append_c [1], for instance in a declaration:
- val append_one = append_c [1]; val append_one = fn : int list -> int listNote that the system does not evaluate the expression append_c [1], because it is a function. It only computes its type. The evaluation will be performed only when we provide also the second argument. For instance:
- append_one [5,6]; val it = [1,5,6] : int list
Consider the functions sum_all : int list -> int and product_all : int list -> int (respectively sum and product of all the elements in a list of integers). They can be defined as follows:
- fun sum_all [] = 0 | sum_all (x::l) = x + sum_all l; val sum_all = fn : int list -> int - fun product_all [] = 1 | product_all (x::l) = x * product_all l; val product_all = fn : int list -> intNote that these two function work according to the same scheme: they scan the list (recursively) element by element, and perform a certain operation on every element (and on the result of the recursive call), and give a certain initial result when the list is empty.
This scheme is common to several other functions. We could then think of defining an abstract function (abstract wrt the operation and the initial element), which represent the general scheme. The particular functions (like sum_all and product_all can then be defined by providing the particular operation and initial value. This general function is commonly called reduce, and it is "more natural" to define it by using currying, as follows:
fun reduce f v [] = v | reduce f v (x::l) = f(x, reduce f v l); val reduce = fn : ('a * 'b -> 'b) -> 'b -> 'a list -> 'bHere, f: 'a * 'b -> 'b represents the operation, and v:'b represents the initial value.
The definitions of sum_all and product_all can now be given as follows:
- val sum_all = reduce (op +) 0; val sum_all = fn : int list -> int - val product_all = reduce (op * ) 1; val product_all = fn : int list -> intWe need to use (op +) and (op * ) instead of + and * because the latter are infix, while in the definition of reduce the parameter f is prefix. The operator op changes a function from infix to prefix.
We can now apply these functions to lists of integers, as illustrated in the following examples:
- sum_all []; val it = 0 : int - sum_all [1,2,3,4]; val it = 10 : int - product_all []; val it = 1 : int - product_all [1,2,3,4]; val it = 24 : intAnother example of function that we can define by using reduce is the function forall, which checks whether a certain property p: 'a -> bool holds for all elements of a list (of type 'a list). We can define it as follows:
fun forall p = let fun f(x,b) = p x andalso b in reduce f true end; val forall = fn : ('a -> bool) -> 'a list -> boolExamples of uses (note that (fn x => x>0) represents the property of being positive; (fn x => x mod 2 = 0) represents the property of being even).
- forall (fn x => x>0) [1,2,3]; val it = true : bool - forall (fn x => x>0) [~1,2,3]; val it = false : bool - forall (fn x => x mod 2 = 0) [2,0,4]; val it = true : bool - forall (fn x => x mod 2 = 0) [2,1]; val it = false : boolOther examples can be found in Assignment 8 (CSE 428, Spring 99). Note that in the assignment the type of reduce is restricted. The reason is due to a certain limitation of the type system of the present implementation of SML. We won't go into that because it's a bit complicated, and not so important.
The concept of currying extends naturally to arbitrary tuples of arguments.
Exp ::= Ide (identifiers) | Exp Exp (functional application) | fn Pattern => Exp (functional abstraction) | (Exp,Exp) (pairing) | Exp :: Exp (cons on lists) | hd Exp (head of a list) | tl Exp (tail of a list) | nil (empty list) Pattern ::= Ide | (Ide,Ide)This is a very small subset of ML (both wrt the expressions and the patterns), and we may wish to extend it later, so to include other interesting data types and constants (like numbers). For the moment however we prefer focussing on just few constructs.
The types of this language constitute a language (type expressions) described by the following grammar:
Type ::= TVar (type variables, i.e. parametres for types) | Type * Type (Cartesian product, the type of pairs) | Type -> Type (functional type, the type of functions) | Type list (the type of lists)We will use the Greek letters alpha, beta, gamma,... to represent the type variables. They correspond to the dashed symbols 'a, 'b, 'c, ... used by the ML system.
Convention: * is left associative. -> is right associative. list has precedence wrt *, and * has precedence wrt ->.
Before studying the Type System formally, let us see some examples of type inferences that are done automatically by the type system of ML.
- fn f => fn x => fn y => f x y; val it = fn : ('a -> 'b -> 'c) -> 'a -> 'b -> 'c
- fn f => fn (x,y) => f(x,y); val it = fn : ('a * 'b -> 'c) -> 'a * 'b -> 'c
- fn f => fn x => fn y => (f x,f y); val it = fn : ('a -> 'b) -> 'a -> 'a -> 'b * 'b
- fn l => fn f => (f (hd l)) :: (tl l); val it = fn : 'a list -> ('a -> 'a) -> 'a list
alpha -> beta -> gamma -> deltawhere alpha is the type of f, beta is the type of x, gamma is the type of y, and delta is the type of the result.
Now, let us analyze how these types are related. In the resulting expression, f is applied to x and the result is applied to y (remember that f x y = (f x) y because application is left-associative). Hence alpha = beta -> phi where phi is the type of (f x). Since we then apply (f x) to y, and we have called delta the type of the result, we must have phi = gamma -> delta.
In conclusion the type is:
(beta -> gamma -> delta) -> beta -> gamma -> delta(names are not important, only their relation is)
We need to put parentheses around the first beta -> gamma -> delta because if we don't do that then the type becomes
beta -> gamma -> delta -> beta -> gamma -> deltawhich is interpreted as
beta -> (gamma -> (delta -> (beta -> (gamma -> delta))))since -> is right-associative.
fn f => fn (x,y) => f(x,y) is a function, where the second parameter is a pair, hence its type must be of the form
alpha -> (beta * gamma) -> deltawhere f: alpha, x: beta, y: gamma, and (f(x,y)): delta
Since f is applied to (x,y) and the result is of type delta, then it must be
alpha = (beta * gamma) -> deltahence the type is
((beta * gamma) -> delta) -> (beta * gamma) -> delta(the parentheses around (beta * gamma) are not necessary because * has priority wrt ->)
alpha -> beta -> gamma -> deltawhere f: alpha, x: beta, y: gamma, and (f x, f y): delta
Now, since the result is a pair, we must have delta = phi * psi, where (f x): phi and (f y): psi. Since f is applied to x and to y, we must have alpha = beta -> phi, and also alpha = gamma -> psi. Hence we obtain beta = gamma, and phi = psi. Thus the type is (again, names are not important):
(beta -> phi) -> beta -> beta -> phi * phi
alpha -> beta -> gammawhere l: alpha, f: beta, and ((f (hd l)) :: (tl l)) : gamma.
Since the result is constructed with a cons operation, it must be a list. Hence we have gamma = delta list, where delta is the type of (f (hd l)). Furthermore, the rest of the list is given by (tl l), hence also l must have type delta list (l and (tl l) have the same type). Therefore we have alpha = delta list. Finally, observe that (hd l): delta and (f (hd l)): delta, hence we can deduce f: delta -> delta. In conclusion, the resulting type is:
delta list -> (delta -> delta) -> delta list