CSE341 Notes for Monday, 4/13/09

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
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
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 pointed out how you can define a function called factors that filters the numbers 1 through n for the factors of a number:

        >>> def factors(n):
        ...     return filter(lambda m : n % m == 0, range(1, n+1))
        ...
This gives us a list of the factors of a number, as in:

        >>> factors(24)
        [1, 2, 3, 4, 6, 8, 12, 24]
        >>> factors(30)
        [1, 2, 3, 5, 6, 10, 15, 30]
        >>> factors(47)
        [1, 47]
Using this, we can use a very straightforward definition of a prime and say that a prime is a number whose factors are 1 and itself:

        >>> def isPrime(n):
        ...     return factors(n) == [1, n]
        ...
And using this we can do familiar things like finding all primes up to 1000:

        >>> filter(isPrime, range(1, 1001))
        [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]
Python doesn't quite have ML's ability to refer to operators by saying op+ or op*, but you can do something very close by importing from a library called operator which then gives you named versions of these operators as functions:

        >>> from operator import *
        >>> mul
        <built-in function mul>
        >>> add
        <built-in function add>
        >>> mod
        <built-in function mod>
We can then make calls on reduce using these operators. For example, factorial can be defined as:

        >>> def factorial(n):
        ...     return reduce(mul, range(1, n+1))
        ...
        >>> factorial(4)
        24
        >>> factorial(5)
        120
And we can use map to get a list of factorials:

        >>> map(factorial, range(1, 31))
        [1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800, 39916800, 479001600,
        6227020800L, 87178291200L, 1307674368000L, 20922789888000L, 355687428096000L,
        6402373705728000L, 121645100408832000L, 2432902008176640000L,
        51090942171709440000L, 1124000727777607680000L, 25852016738884976640000L,
        620448401733239439360000L, 15511210043330985984000000L,
        403291461126605635584000000L, 10888869450418352160768000000L,
        304888344611713860501504000000L, 8841761993739701954543616000000L,
        265252859812191058636308480000000L]
Unlike ML, Python by default uses "infinite" integers instead of generating an overflow for large values.


Stuart Reges
Last modified: Thu Apr 16 16:42:43 PDT 2009