CSE 130, Summer Session I, 2008

Programming Languages: Principles and Paradigms

Lectures


2008-06-30: Introduction to Functional Programming in Scheme, Part 1


2008-07-01: Introduction to Functional Programming in Scheme, Part 2


2008-07-02: Recursion and Higher-Order Functions

Here are some simple examples of somewhat analogous applications of map and foldl:

> (map + '(1 2 3))
(1 2 3)
> (foldl + 0 '(1 2 3))
6
> (map string-append '("a" "b" "c"))
("a" "b" "c")
> (foldl string-append "" '("a" "b" "c"))
"cba"

Both take a procedure and a list, but map treats the procedure as unary, applies it to each element, and returns a list of the results; whereas foldl treats the procedure as binary, combines the results using the procedure itself, and returns a single result.

Because it treats the procedure as binary, foldl requires an initial value to combine with the first element. In the examples above, the initial values are zero and the empty string, respectively. After applying the procedure to the first element and the initial value, it proceeds to apply the procedure to each element and the result of the previous application until the list is consumed, and then returns a final result from the chain of applications.

The versions of map and foldl in the PLT library can actually handle multiple lists, but here's how you can define simple versions of each that handle just one list, as in the examples above:

(define (map-1 proc lst)
  (if (empty? lst) empty
      (cons (proc (first lst))
            (map-1 proc (rest lst)))))

(define (foldl-1 proc val lst)
  (if (empty? lst) val
      (foldl-1 proc
               (proc (first lst) val)
               (rest lst))))

Exercises for self-enrichment:

  1. What is the result of (map-1 list '(a b c))? What about (foldl-1 list '() '(a b c))?
  2. Is map-1 tail recursive? Is foldl-1?

2008-07-03: Tail Recursion

Consider the following definitions:

(define (sum2 a b) (+ a b))

(define (sum . lst)
  (if (empty? lst) 0
      (sum2 (first lst) (apply sum (rest lst)))))

(define (sum-tr . lst)
  (let helper ((partial-sum 0) (h-lst lst))
    (if (empty? h-lst)
        partial-sum
        (helper (sum2 (first h-lst) partial-sum) (rest h-lst)))))

Now sum2 takes exactly 2 parameters, but both sum and sum-tr recursively compute the sum of any number of parameters. But sum-tr is tail recursive: the value of each recursive call to sum-tr is returned directly without further computation. Because the value of the last recursive call is the value of the entire recursion, the activation records (or stack frames) don't need to be retained, so the entire recursion can be computed without the stack growing. (You can see this when step-evaluating in the debugger, as we did in class.)

Here are simple recursive and tail-recursive functions that compute Fibonacci numbers:

(define (fib n)
  (cond
    ((= n 0) 0)
    ((= n 1) 1)
    ((> n 1) (+ (fib (- n 1)) (fib (- n 2))))))

(define (fib-tr n)
  (cond
    ((= n 0) 0)
    ((> n 0)
     (let helper ((i 1) (fib_i 1) (fib_i-1 0))
       (if (= i n) fib_i
           (helper (+ i 1) (+ fib_i fib_i-1) fib_i))))))

The second function is tail-recursive, which saves space; it is also singly recursive instead of multiply recursive, which saves time. For me the difference in running time became noticeable for n about 30.


2008-07-07: Substitution and Function Application

Discussion of issues in second and third programming assignments, also covered in chapters 3 and 4 of PLAI.


2008-07-08: Parameter-Passing Mechanisms

Here's a working example of call-by-name parameter passing in Algol, which you can run in DrScheme using the Algol 60 language:

begin
    integer i;

    procedure power(v,expr,n);
    begin
        integer i;
        for i:=1 step 1 until n do
        begin
            v:=expr
        end
    end;

    i:=1;
    power(i,i*2,8);
    printn(i)

end

2008-07-09: Deferred Substitution, Closures, and Currying

Now we return to currying, and emphasize the role closures play in defining generalized curried functions. In the following we assume deferred substition is in effect. First, let's take a look at a curried function that adds 2 parameters:

(define curried-sum2
  (lambda (x)
    (lambda (y)
      (+ x y))))

When the outer lambda is invoked, its formal parameter x is bound to a value, and the inner lambda is returned. The inner lambda needs the value of x when it is invoked, but the activation record for the outer lambda is gone.

In order for this to work, a procedure value (like the internal lambda's) must contain not only its own formals and body, but also the environment in which it originated. This environment contains bindings in scope lexically where the procedure is defined. When the procedure is applied, bindings for its formal parameters are added to this environment before its body is evaluated in that context. The formals and body together with the environment is called a closure.

Now let's look at a curried function that adds 3 parameters:

(define curried-sum3
  (lambda (x)
    (lambda (y)
      (lambda (z)
        (+ x y z)))))

When each of the first 2 lambdas above is invoked, it returns a closure containing the formals and body of the next lambda, and an environment in which its own formal is bound. This way when the body of the last lambda is evaluated, it has an environment with bindings for all 3 identifiers it needs.

Can we generalize this currying process? We need to take a procedure proc that takes n parameters, and give a procedure that takes one parameter and gives another procedure that takes another parameter, and so on until n parameters have been bound in this way. The last procedure in this chain should invoke proc on all n parameters. Drawing on our earlier examples, here's how we'll accomplish this:

(define (curry-n proc n)
  (let collect-args ((reversed-args empty) (num-collected 1))
    (lambda (arg)
      (if (= num-collected n)
          (apply proc (reverse (cons arg reversed-args)))
          (collect-args (cons arg reversed-args)
                        (+ num-collected 1))))))

We have to specify the number of parameters explicitly because we can't determine from a Scheme procedure value alone how many parameters it takes, and we need to know when to stop. Because in this generalized form each successive lambda will have a formal parameter with the same name, we define a "helper function" collect-args to store the parameters in a list. Although collect-args is in scope only within curry-n, each lambda has collect-args, and its formals, bound in the environment of the resulting closure. This is important because each lambda uses those formals, and each until the last also invokes collect-args to return the next lambda.

Now we can see how to define a fully curried sum that stops when applied to the empty list (just like Scheme's +). Because addition is associative, we can also simplify the process by adding at each step and storing a partial sum instead of a list:

(define curried-sum
  (let add-another ((partial-sum 0))
    (lambda args
      (if (empty? args) partial-sum
          (add-another (apply + (cons partial-sum args)))))))

As an exercise, try generalizing the uncurrying process: given a curried procedure, return an uncurried procedure that takes all its arguments at once. Consider: does the number of parameters need to be supplied up front, as it did with uncurry-n above?


2008-07-10: Haskell

See also the tutorial A Gentle Introduction to Haskell for a somewhat more in-depth treatment, and the Haskell Reference at ZVON.org when you want to look up something specific.


2008-07-14: Outro to Laziness and Intro to Types


2008-07-15: Operational Semantics


Valid XHTML 1.0 Strict Valid CSS!