Up to now we've been writing functions of two arguments using a tuple, as in:
- fun sum(x, y) = x + y; val sum = fn : int * int -> intNotice the response from ML. We know from the "->" arrow that we have a function. It converts an int * int tuple into an int. It turns out that the parentheses are not needed. We can instead say:
- fun sum x y = x * y; val sum = fn : int -> int -> intThis is the curried version of the function. Notice the response this time. We have two arrows ("->"). ML is telling us that this version of sum is a function that takes just a simple int as an argument. That function returns a function that maps an int into an int. So instead of thinking of it as a single function that takes a tuple, we think of it as a function that returns a function. When we call it, we don't use parentheses and ML evaluates it from left to right:
sum 3 5 = (sum 3) 5 = (a function that adds 3 to something) 5 = 8Notice how in evaluating (sum 3), ML produces a new function. This new function is then applied to the number 5. Someone asked why a curried function is helpful. I said that it allows you to partially instantiate a function. For example, suppose you want a function for doubling a number. We can say:
fun double(n) = 2 * n;But let's see what we could do with a curried multiply function:
- fun multiply x y = x * y; val multiply = fn : int -> int -> intNotice that ML says that if we give one int to multiply, we'll get back a new function. So we can form an expression for computing a new function. Since it's an expression, we use a val declaration rather than a fun declaration:
- val double = multiply 2; val double = fn : int -> intWe use a val declaration rather than a fun declaration because we are writing an expression that evaluates to a function rather than introducing our own definition for a function.
As a further example, I mentioned that I have included curried versions of map, filter and reduce in utility.sml with the names map2, filter2 and reduce2. For example, if we want a function that converts a list of ints to a list of reals, we could use the old map:
- fun toReals(lst) = map(real, lst); val toReals = fn : int list -> real listThat works, but with the curried map2 function, we can write this more simply as an expression:
- val toReals = map2 real; val toReals = fn : int list -> real listI mentioned that when we say things like "op +" or "op *" to get a functional version of an operator that the result is an uncurried function. I've written a short function that converts an uncurried function like this into a curried function. It is included in utility.sml:
fun curry f x y = f(x, y);We can use an expression like this to get a curried version of the addition operator:
- curry op+; val it = fn : int -> int -> intI've removed the space between "op" and "+", although that has no effect on the computation. I find it a little easier to read it in this form when I'm passing the operator as an argument to the curry function. Using this function, we could have written the double function this way:
- val double = curry op* 2; val double = fn : int -> intThen we turned to a more complex example. On homework assignment 1 there was a problem to write a function that would return the number of negatives in a list. That's a little too close to a problem on the current homework, so I said that we'd write a function to return the number of zeros in a list.
The function filter is very helpful here. We just need a function to determine if a value is equal to 0. This is a good place to use an anonymous function:
fn x => (x = 0)We can use that to filter a list:
filter(fn x => (x = 0), lst)We then return the length of this list as the answer:
fun numZeros(lst) = length(filter(fn x => (x = 0), lst))Then we explored how to use currying to replace the anonymous function with an equivalent expression. What is the underlying function that we're applying? The operator equals. If you just refer to:
op=you get a noncurried version of the function. We can pass this as an argument to the curry function that I've included in utility.sml to produce a curried version of the operator:
curry op=Now we need to partially instantiate the function. The problem is that our test begins with x:
x = 0But it doesn't have to begin with 0, because this is equivalent:
0 = xThis is now fairly easy to partially instantiate:
curry op= 0When we typed the expression above into the ML interpreter, it responded with this:
val it = fn : int -> boolJust as we would hope, the expression evaluates to a function that takes an int as an argument and that returns a boolean value (whether or not the int is equal to 0). I mentioned that in writing ML code, it is useful to type these little code snippets into the interpreter to check your solution. If you rely on always typing in the full version of a function, you might have trouble locating syntax errors or bugs. It's better to test it in pieces.
Using this expression, we were able to define a new version of the function:
fun numZeros2(lst) = length(filter(curry op= 0, lst))Then I asked how we could use currying to eliminate the function definition and replace it with a val declaration. The problem is that we have two different functions that we want to apply: length and filter. Whenever you have two or more functions to apply, you know you're going to need the composition operator. So the basic form of our answer is going to be:
val numZeros3 = length o (call on filter)In other words, at the highest level what we're doing is to compose a call on length with a call on filter. But how do we rewrite the call on filter? In the code above, we are using the noncurried version of filter. I have included in utility.sml curried versions of the higher-order functions called map2, filter2 and reduce2. The general form of a call on filter2 would be:
filter2 (predicate function) (list)In our case, we are trying to eliminate the list parameter so that we can write this using a val declaration rather than a standard function declaration. In other words, we want a partially instantiated call on this function where we supply the predicate but not the list. We have to use filter2 instead of filter and we have to be careful to use parentheses to indicate the grouping of the expression that returns our predicate function:
filter2 (curry op= 0)Ullman has good examples in the section on curried functions in chapter 5 about how to properly use parentheses in an expression like this. Without the parentheses, ML tries to think of this as:
(filter2 curry) op= 0And that generates an error because the curry function is not a predicate. Putting the filter2 expression into our original expression, we get our third version of the function definition:
val numZeros3 = length o (filter2 (curry op= 0))In this case we actually don't need parentheses around the call on filter2, but it's generally easier to include some extra parentheses than to have to learn all of the subtleties of ML precedence rules.
I ended by spending a few minutes in the Python interpreter to demonstrate that many of these ideas have crossed over into Python. The Python interpreter has the functions map, filter, and reduce, and it will tell you so:
attu3% python Python 2.5.1 (r251:54863, Jun 15 2008, 18:24:51) [GCC 4.3.0 20080428 (Red Hat 4.3.0-8)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> map <built-in function map> >>> filter <built-in function filter> >>> reduce <built-in function reduce>Python has a function called "range" that serves the same purpose as our -- operator. If you ask for range(n) you get the numbers 0 through (n-1). If you ask for range(m, n) you get the numbers m through (n-1):
>>> range(10) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>> range(1, 11) [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]Using map, you can do the kinds of computatins we've been doing, like finding the square roots of the numbers 1 through 10:
>>> from math import * >>> map(sqrt, range(1, 11)) [1.0, 1.4142135623730951, 1.7320508075688772, 2.0, 2.2360679774997898, 2.4494897427831779, 2.6457513110645907, 2.8284271247461903, 3.0, 3.1622776601683795]You can also declare an anonymous function using the keyword "lambda":
>>> map(lambda n : 2 * n, range(1, 11)) [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]The notation is very similar to ML's. We are saying that we want an anonymous function (a lambda) that maps n into (2 * n). Python separates those with a colon rather than an arrow, but the idea is the same.
I said that we'd look at more Python examples in the next lecture.