CSE341 Notes for Monday, 10/12/09

We began with the filter function, which takes a predicate function (a boolean test) and a list as arguments and that returns the list of values that satisfy the given predicate. We write it this way:

        fun filter(f, []) = []
        |   filter(f, x::xs) =
                if f(x) then x::filter(f, xs)
        	else filter(f, xs)
Given this function and the isPrime function we wrote in section, I wrote this expression to request a list of all primes up to 1000:

        - filter(isPrime, 1--1000);
        val it =
          [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,
           103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,
           197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,
           307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409,
           419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,503,509,521,
           523,541,547,557,563,569,571,577,587,593,599,601,607,613,617,619,631,641,
           643,647,653,659,661,673,677,683,691,701,709,719,727,733,739,743,751,757,
           761,769,773,787,797,809,811,821,823,827,829,839,853,857,859,863,877,881,
           883,887,907,911,919,929,937,941,947,953,967,971,977,983,991,997] : int list
Then we looked at the reduce function which collapses a list to a single value given a function that collapses two values from the list into a single value. At first this might sound unusual, but you'll find that we have many such collapsing operations. For example, the addition operator takes two numbers and turns them into one number. The reduce function does two at a time to an entire list, until the list has been reduced to a single value:

        exception empty_list;
        fun reduce(f, []) = raise empty_list
        |   reduce(f, [x]) = x
        |   reduce(f, x::xs) = f(x, reduce(f, xs));
For example, this expression asks ML to reduce the list of integers 1 through 100 to a single value using addition:

        - reduce(op +, 1--100);
        val it = 5050 : int
We computed 5! by collapsing the list 1 through 5 with multiplication:

        - reduce(op *, 1--5);
        val it = 120 : int
I asked people for other examples of collapsing operations and we came up with a rather extensive list:

The following table gives a summary of map, filter and reduce.

function 1st argument 2nd argument returns
map function mapping 'a to 'b list of n 'as list of n 'bs
filter predicate converting 'a to bool list of n 'as list of m 'as (m <= n)
reduce function that collapses a tuple
'a * 'a into a single 'a
list of n 'as one 'a

I then spent a few minutes discussing the fact that the reduce function has disappeared from the standard ML libraries. We still talk about the idea of a reduce function, but there were too many problems with the standard reduce. First, it doesn't handle empty lists well. The newer versions of reduce include an extra parameter that indicates a "default value" to use for the computation when there is no data to process (the answer for an empty list). For example, when you are adding, the default value is 0. When you are multiplying, the default value is 1. Second, we sometimes want to make a distinction between processing the list from left-to-right versus processing the list from right-to-left. For many computations it doesn't matter, but when it does matter, it's nice to be able to control which direction it uses. Finally, reduce always reduces to a value of the same type as the original list. For example, lists of int values are reduce to a single int. Lists of strings are reduced to a single string. Sometimes you want to use a reducing function that reduces to some other kind of value.

The new terminology involves thinking of this as a "folding" operation, so the two replacement functions are known as foldl and foldr (fold from the left or fold from the right). We're written reduce to fold from the right. For example, the following are equivalent:

        - reduce(op ^, ["four", "score", "and", "seven", "years", "ago"]);
        val it = "fourscoreandsevenyearsago" : string
        - List.foldr op^ "" ["four", "score", "and", "seven", "years", "ago"];
        val it = "fourscoreandsevenyearsago" : string
The call on List.foldr includes an extra parameter of an empty string (default value). To make this even more clear, I used the string "foo":

        - List.foldr op^ "foo" ["four", "score", "and", "seven", "years", "ago"];
        val it = "fourscoreandsevenyearsagofoo" : string
Notice that the values from the list are concatenated with "foo" starting with the rightmost value and working backwards towards the first value. We get the text in the opposite order when we call List.foldl:

        - List.foldl op^ "foo" ["four", "score", "and", "seven", "years", "ago"];
        val it = "agoyearssevenandscorefourfoo" : string
The functions List.foldl and List.foldr are defined as curried functions. That's why you don't use parentheses and commas when you call them. We'll discuss that more in the next lecture. I also mentioned that these aren't the standard definitions of map and filter. The standard versions are curried.

I also mentioned that Google has developed a tool that they call MapReduce to help them solve the problem of performing large scale computations in a distributed computing environment (hundreds of computers each handling a small part of the overall computation). The lack of side effects that you get from functional constructs like map and reduce make it easier to parallelize the computation. Google cares so much about this technology that they have developed a course here at the uw that we have numbered 490h. It teaches the same concepts using an open-source implementation of MapReduce known as hadoop. The department expects to continue to offer 490H in the future.

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? This requires applying three different functions. We could do it with three calls to map:

        - map(round, map(Math.sqrt, map(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
This is inefficient because it makes three passes over the data. A better approach is to use an anonymous function:

        map(fn n => round(Math.sqrt(real(n))), 1--100);
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

Stuart Reges
Last modified: Mon Oct 12 20:12:45 PDT 2009