CSE341 Notes for Wednesday, 10/10/07

We covered two remaining major topics from chapter 5 that I ask you to practice in the next programming assignment and we spent a little more time on the higher-order functions map, filter and reduce. There are several small problems to practice these ideas and for those problems, you'll be limited in how to solve the problems. But the remainder of the assignment involves solving a nontrivial task with ML and for that part of the assignment you can use all of ML, including the library functions.

We saw that we can do a double mapping where we first convert a list to reals and then find the square root of each:

- map(Math.sqrt, map(real, 1--100));
val it =
  [1.0,1.41421356237,1.73205080757,2.0,2.2360679775,2.44948974278,
   2.64575131106,2.82842712475,3.0,3.16227766017,3.31662479036,3.46410161514,
   3.60555127546,3.74165738677,3.87298334621,4.0,4.12310562562,4.24264068712,
   4.35889894354,4.472135955,4.58257569496,4.69041575982,4.79583152331,
   4.89897948557,5.0,5.09901951359,5.19615242271,5.29150262213,5.38516480713,
   5.47722557505,5.56776436283,5.65685424949,5.74456264654,5.83095189485,
   5.9160797831,6.0,6.0827625303,6.16441400297,6.2449979984,6.32455532034,
   6.40312423743,6.48074069841,6.5574385243,6.63324958071,6.7082039325,
   6.78232998313,6.8556546004,6.92820323028,7.0,7.07106781187,7.14142842854,
   7.21110255093,7.28010988928,7.34846922835,7.4161984871,7.48331477355,
   7.54983443527,7.61577310586,7.68114574787,7.74596669241,7.81024967591,
   7.87400787401,7.93725393319,8.0,8.0622577483,8.12403840464,8.18535277187,
   8.24621125124,8.30662386292,8.36660026534,8.42614977318,8.48528137424,
   8.54400374532,8.60232526704,8.66025403784,8.71779788708,8.77496438739,
   8.83176086633,8.88819441732,8.94427191,9.0,9.05538513814,9.11043357914,
   9.16515138991,9.21954445729,9.2736184955,9.32737905309,9.38083151965,
   9.43398113206,9.48683298051,9.53939201417,9.59166304663,9.64365076099,
   9.69535971483,9.74679434481,9.79795897113,9.8488578018,9.89949493661,
   9.94987437107,10.0] : real list
You can also map a function you define over a list. For example, the same list above can be computed by saying:
        - fun f(x) = Math.sqrt(real(x));
        val f = fn : int -> real
        - map(f, 1--100);
This is even easier to write with an anonymous function. Ullman describes how to define anonymous functions in section 5.1.3 of the book. The basic syntax involves the keyword "fn" and the two characters "=>" that are supposed to look like an arrow. The general form is: fn <parameter> => <expression> For example, our function f above could be defined as:

        fn x => Math.sqrt(real(x))
Notice that for anonyomous functions you use "fn", not "fun". I read this as, "A function that maps x into Math.sqrt(real(x))."

You can also define an anonymous function on a tuple, as in:

        fn (x, y) => x + y
which I read as, "A function that maps (x, y) into x + y."

Using an anonymous function, we can rewrite our call on map to be:

        map(fn x => Math.sqrt(real(x)), 1--100);
As a challenging problem, I asked people to think about how we could write a function that would return Pascal's triangle. The rows of Pascal's triangle show the coefficients of the expansion of:

(x + y)n
As in:

              1
             1 1
            1 2 1
           1 3 3 1
          1 4 6 4 1
        1 5 10 10 5 1
We'll return it as a list, so that the call triangle(5) should return:

        - triangle(5);
        val it = [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1],[1,5,10,10,5,1]]
          : int list list
The idea is to return the triangle up to row n. There is a row 0 that contains just the number 1. I pointed out that the numbers in Pascal's triangle are known as the binomial coefficients or "n choose m". Ullman includes this as an example in the book:
        (* returns n choose m *)
        fun combin(n, m) =
            if m = 0 orelse n = m then 1
            else combin(n - 1, m - 1) + combin(n - 1, m);
So how do we use this to construct the triangle? Overall the triangle is a list of rows:

        [row 0, row 1, row 2, ..., row n]
I asked how we could get something like that and someone said that we could map over the list 0--n:

        fun triangle(n) = map(fn x => produce row x, 0--n);
This is a good start. It produces a list of the correct length with a structure that will allow us to generate the different rows. Now we just have to figure out how to generate row x for an arbitrary x. I said to consider 3 as an example. It ends up having 4 values. They come from:

        [3 choose 0, 3 choose 1, 3 choose 2, 3 choose 3]
In other words, we want all of the binomial coefficients with 3 as the first argument and the various values 0 through 3 as the second argument. This can also be accomplished with a map. In producing row x, we want to map over the values 0 through x:
        map(fn y => something, 0--x)
To fill in "something", we have to recognize that we want "x choose y", which is simply a call on Ullman's combin function:

        map(fn y => combin(x, y), 0--x)
This is the code for producing row x, so we put it into our previous expression:

        fun triangle(n) = map(fn x => map(fn y => combin(x, y), 0--x), 0--n);
This short function produces the rows of Pascal's triangle. I told people that I think it's challenging to learn to write code this way and when you're first learning, this code can be difficult to read. But I'm always impressed at how concisely I can write some computations that seem very complex. There is certainly no way to solve this problem with one line of code in Java with or without a combin function.

I then considered a short problem. What if you wanted a list of the square roots of the numbers 1 through 100 rounded to the nearest integer? You can use an anonymous function to do so:

- map(fn x => round(Math.sqrt(real(x))), 1--100);
val it =
  [1,1,2,2,2,2,3,3,3,3,3,3,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,
   6,6,6,6,6,7,7,7,7,7,7,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,9,9,
   9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,10,10] : int list
This works, but there is a better way. Veteran ML programmers would say, "Why introduce an anonymous function when you're just combining a set of functions that already exist?" Notice the pattern we have in this function where one function calls the other which calls the other, as in:

f(g(h(x)))
Mathematicians refer to this as composition of functions and ML has an operator that allows you to more easily compose functions. The operator is the lowercase letter "o". So our function above could be rewritten as:

- map(round o Math.sqrt o real, 1--100);
val it =
  [1,1,2,2,2,2,3,3,3,3,3,3,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,
   6,6,6,6,6,7,7,7,7,7,7,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,9,9,
   9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,10,10] : int list
Then we turned our attention to curried functions. I mentioned that this concept seems to lead to some controversy. Some people really appreciate the power you get with curried functions. Others find that it leads them to a level where the air is so thin that they have trouble breathing. In any event, it is an interesting idea to explore and most of the standard ML functions are written this way, so we need to understand it.

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 -> int
Notice 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 -> int
This 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 = 8
Notice 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 -> int
Notice 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 -> int
We 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 list
That 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 list
I 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 -> int
I'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 -> int

Stuart Reges
Last modified: Fri Oct 12 10:09:12 PDT 2007