Lecture 17 Implementing the Substitutional Model of Functions.


1 Implementing the Substitutional Model.
2 Treating recursion in a purely substitutional way.
  2.1   The problem of implementing recursion when we only have lambda
  2.2   The Y combinator is a fixed-point operator.
  2.3   But Y can be written as a lambda expression!
  2.4   Using Y to define recursive functions
3 Building an interpreter for a functional Scheme-like language
  3.1   Associations support arbitrary finite mappings
  3.2   A Scheme Interpreter: eval_eager is the eager, substituting version
  3.3   eval_compound evaluates special forms, and any application
      ... Finding the method for a special form
      ... The definition of eval_compound using method-lookup.
      ... Defining the method to evaluate the special form lambda
      ... Defining the method to evaluate the special form if
      ... Defining the method to evaluate the Y combinator.
      ... Defining the method for quoted constants
      ... Testing out eval_eager
  3.4   Defining apply_eager  to implement the application of a function.
      ... Applying primitives such as +, *, car, cons, cdr
      ... The implementation of apply_eager
      ... Examples of the use of apply_eager
  3.5   Implementing the application of a compound function to arguments
      ... Implementing the application of a function of the form (Y- e)
      ... Implementing the application of a lambda expression
      ... Implementing the substitution of  values for variables
      ... Making a substitution in a lambda-expression
4 Shonfinkel's Combinators enable us to dispense with lambda

1 Implementing the Substitutional Model.

Our understanding of the purely functional use of Scheme was expressed in terms of the substitution model of evaluation. This substitution model says

For example, the expression:

    ( (lambda (x y) (+ x (* 2 y)))  4 5)

    ==> (+ 4 (* 2 5))

    ==> (+ 4 10)

    ==> 14

This model assumes that there is some built-in code for recognising the what to do with primitives such as + *.

[Note: The original definition of the lambda calculus, devised by Alonzo Church in the 1930's employed substitution as the basic way of simplifying an application].

2 Treating recursion in a purely substitutional way.

Mathematics and science have progressed by the application of Occam's Razor, non sunt multiplicanda entia praeter necessitatem. Computer scientists who have ambitions to be more than hackers of arbitrary chunks of code might do well to pay attention to this principle, for it the right approach to creating software systems that are engineered in the sense that they are subject to formal analysis.

For the functional paradigm the theoretical basis is the lambda calculus, which provides a substitutional model. Applying Occam's Razor, we would wish to shave off as many special forms as possible by explaining how they can be transformed into more basic forms. In fact we can get rid of all of them except the lambda construct itself.

2.1 The problem of implementing recursion when we only have lambda

In a mathematical development of the theory of the substitional model we have a problem raised by recursion. Compare

(define  double (lambda (x) (* 2 x)))

(define sum
    (lambda (l) (if (null? l) 0 (+ (car l) (sum (cdr l)))))
)

In the case of double what we are doing can be regarded as binding a variable. Indeed we could write a program like:

    (define  double (lambda (x) (* 2 x)))

    (double 3)

As
    ((lambda (double) (double 3)) (lambda (x) (* 2 x)))

All Scheme programs which do not involve recursion and which do not redefine functions can be systematically treated in this way. That is to say, we do not need to regard define as any special construct, but merely as something that can be transformed into a lambda construct.

However the definition of sum is not so simple, since sum occurs recursively in the lambda expression. Consider:

 (
  (lambda (sum) (sum '(1 2 3)))
  (lambda (l)
      (if
          (null? l)
          0
          (+ (car l) (sum (cdr l)))
          )
      )
  )

Error: Cannot apply object Uninitialised variable 'sum as function In file: /users/users3/fac/pop/poplocal/local/Scheme/lecture14.scm

The second lambda expression has sum as an unbound free variable. (Note that the second lambda expression is not contained within the first, so that the sum of the second lambda expression is not in the scope of the sum of the first).

We could get over this difficulty by giving up on trying to express define in terms of lambda, and regarding it as specifying a recursion equation, whose solution was the required recursive function, for example, sum could be regarded as a solution of:

  (sum x) = (if (null? x) 0 (+ (car l) (sum (cdr x))))

2.2 The Y combinator is a fixed-point operator.

However it is not necessary to do this! Instead we introduce the Y-combinator, with the property that, for any function e:

     (Y e)  = (e (Y e))

We say that Y is a fixed point operator, that is (Y e) has the property that when we apply e to it, it remains unchanged. The concept of a fixed point is an important one in mathematics and physics. Consider for example, a rotating body. The rotation can be considered as a mapping from where the body was originally to where it is now. The fixed point of this mapping is the axis of rotation. More generally, in Linear Algebra, we have the concept of an eigenvector.

In engineering generally, the design of a process for making an artefact very often consists of finding an operation whose fixed point is the artefact, or at least a feature of it. Thus the fixed point of the operation of planing is a planed surface. So, if we need a plane surface we think of planing as a way of achieving it. Likewise the fixed point of the operation of screw-cutting is a threaded surface.

So, it is not surprising in Computer Science that a fixed-point operator is important in constructing the artefacts that we produce - computer programs.

You might also say that e is a symmetry of (Y e), and this is a clue to why it works. Observe that the data-structures we construct in Scheme are self similar, or in a sense, symmetric. [Note however that in Mathematics it is usual to regard symmetry operators as being invertible - if you rotate a body you can rotate it back. Computation generally throws away information, so is not invertible]. When, for example, you take the cdr of a list, you still have a list. Recursion works by exploiting this symmetry. The notes on the lambda calculus available online discuss Y in a more mathematical framework.

2.3 But Y can be written as a lambda expression!

Now, in introducing Y we are not introducing a new construct, for it can be written in Scheme notation as:

 (lambda (h)
     (
      (lambda (x) (h (x x)))
      (lambda (x) (h (x x)))
      )
     )
[This definition is due to Alan Turing]

Let's see how this works - suppose we have some arbitrary expression e. Applying Y to e

  (Y e) ==>   [use value for Y given above]
   (
     (lambda (h)
         (
          (lambda (x) (h (x x)))
          (lambda (x) (h (x x)))
          )
         ) e)

==>  [substitute e for h in the body of the (lambda (h) ..) function, strip
      off lambda (h) ]

         (                                         [1]
          (lambda (x) (e (x x)))
          (lambda (x) (e (x x)))
          )

==> [substitute the second (lambda (x) ...) expression for x in the body of
     the first (lambda (x) ...) expression, strip off lambda.

    (e (                                           [2]
        (lambda (x) (e (x x)))
        (lambda (x) (e (x x))))
        )

But what we have here is two expressions, [1] and [2] for (Y e), but the second is of the form (e (Y e)).

2.4 Using Y to define recursive functions

The Y combinator is used in theory to create recursive functions by providing a binding for the unbound recursive call that plagued us above.

  (lambda (sum)
    (lambda (l)
        (if
            (null? l)
            0
            (+ (car l) (sum (cdr l))))
        )
    )
in:
 (Y
   (lambda (sum)
       (lambda (l)
           (if
               (null? l)
               0
               (+ (car l) (sum (cdr l))))
           )
       )
   )

This expression is the sum function that we are trying to define recursively.

So, if we wanted to calculate (sum '(1 2)), we would simply apply our expression thus:

 (
  (Y
     (lambda (sum)
         (lambda (l)
             (if
                 (null? l)
                 0
                 (+ (car l) (sum (cdr l))))
             )
         ) ; end of E
     )     ; end of (Y E), that is the sum function.
  '(1 2))  ; the argument of (Y E)

The way that this works as a recursive definition is by making use of the identity (Y E) = (E (Y E)). Applying the identity we obtain:

    (
     (                                         ; start of (E (Y E))
      (lambda (sum)                            ; start of E
          (lambda (l)
              (if
                  (null? l)
                  0
                  (+ (car l) (sum (cdr l))))
              )
          )                                     ; end of E
      (Y                                        ; start of         (Y E)
         (lambda (sum)
             (lambda (l)
                 (if
                     (null? l)
                     0
                     (+ (car l) (sum (cdr l))))
                 )
             )                                   ; end of E
         )                                       ; end of (Y E)
      )                                          ; end of (E (Y E))
     '(1 2))                                     ; the argument of (E (Y E))

We can now use the fact that E, that is the (lambda (sum) ... ) expression, is applied to the (Y (lambda (sum)...)) expression; using the rule about a lambda expression being applied to an argument, we substitute (Y E) for sum in the body of E [remember that the (Y E) is the sum function (we hope!)]. That is, wherever we have the "recursive" call of sum, we may replace sum by the (Y (lambda (sum)...)) expression. The fact that we can do this, and do it as often as we need to, is the secret of how Y works.

==>


    (
     (lambda (l)
         (if
             (null? l)
             0
             (+ (car l)
                (                           ; (Y E) replaces sum
                 (Y                         ; start of         (Y E)
                    (lambda (sum)
                        (lambda (l)
                            (if
                                (null? l)
                                0
                                (+ (car l) (sum (cdr l))))
                            )
                        )                                  ; end of E
                    )                                      ; end of (Y E)
                 (cdr l)))                                 ;
             )                                             ; end if
         )                                                 ; end (lambda(l)..)

     '(1 2))                                 ; the argument of (lambda (l)...)


You can see what has happened - the expression (Y e) has been pulled inside the lambda-expression that we created for sum, indeed substituting for the recursive call of sum. That is if (Y e) is indeed the desired sum function, it is behaving in the right way!. We can now reduce the outer lambda expression:

    (if
        (null? '(1 2))
        0
        (+ (car '(1 2)) (
            (Y (lambda (sum)         ; start of  (Y E)
                   (lambda (l)
                       (if
                           (null? l)
                           0
                           (+ (car l) (sum (cdr l))))
                       )
                   )

               (cdr '(1 2)))))
        )
    )

We can now do some straightforward computation. The expression (null? '(1 2)) evaluates to #f, and (cdr '(1 2)) evaluates to '(2) so applying the rules for if we obtain:

  ==>

  (+ 1 (
    (Y (lambda (sum)
           (lambda (l)
               (if
                   (null? l)
                   0
                   (+ (car l) (sum (cdr l))))
               )
           )

       '( 2))))
        )

We can now use (Y E) = (E (Y E)) all over again, obtaining:

  (+ 1
    (if
        (null? '( 2))
        0
        (+ (car '( 2)) (
            (Y (lambda (sum)
                   (lambda (l)
                       (if
                           (null? l)
                           0
                           (+ (car l) (sum (cdr l))))
                       )
                   )

               (cdr '( 2)))))
        )
    )
  )

One further use of (Y E) = (E (Y E)) gives us:

  (+ 1
   (+ 2
      (if
          (null? '())
          0
          (+ (car '()) (
              (Y (lambda (sum)
                     (lambda (l)
                         (if
                             (null? l)
                             0
                             (+ (car l) (sum (cdr l))))
                         )
                     )

                 (cdr '()))))
          )
      )
   )

Now, because (null? '()) evaluates to #t, the (if..) expression evaluates to 0. So the whole expression evaluates to the simple form:

  (+ 1
   (+ 2 0))

    ==> 3

Note that, while the definition of the Y combinator can be written in Scheme, we cannot get it to work correctly in Scheme itself, since Scheme uses eager evaluation and will give rise to recursive run-away We treat the application of Y to its argument in a lazy way to obtain a correct reduction.

Suppose we define Y as:


(define Y
    (lambda (h)
        (
         (lambda (x) (h (x x)))
         (lambda (x) (h (x x)))
         )
        )
    )

And try to evaluate [paste this into a file 'temp.scm']

    (Y (lambda (sum) (lambda (l) (if (null? l) 0 (+ (car l) (sum (cdr l)))))))
we obtain the error message:
Error: rle: RECURSION LIMIT (pop_callstack_lim) EXCEEDED
In file: /users/users3/fac/pop/temp.scm

3 Building an interpreter for a functional Scheme-like language

Now that we have defined the Y-combinator, we can try to write an interpreter for a purely-functional Scheme-like language which uses lambda as a way of creating functions together with the Y combinator for recursion. We will use Y- to denote this combinator, to avoid confusion with the the identifier Y which is a useful name we would not want to have as a special form.

This enterprise may seem like pulling ourselves up by our own bootstraps, but it is actually a vary good way of characterising a language precisely, and a good start to a portable implementation.

In interpreting our formalism we have two choices for the evaluation strategy: eager evaluation in which the actual parameters of a function-application are evaluated before substitution, and lazy evaluation in which they are not evaluated before substitution. In fact, with lazy evaluation, expressions are only evaluated when there is an actual need for their values, for example when we need to print them out. The Scheme language itself is defined to use eager evaluation, but we could define a lazy version (indeed there is one called Sloth!) which ran lazily.

3.1 Associations support arbitrary finite mappings

Any consideration of the evaluation of expressions requires us to have some way of associating a variable with a value. The conventional way to handle this requirement in Scheme is to use an association list, commonly called an alist. This consists of a list of attribute-value pairs. For example:

     '((a . 3) (x . 2))

associates a with the value 3 and x with the value 2. We sometimes say that the variable a is bound to the value 3 and the variable x is bound to the value 2.

The standard Scheme functions assoc, assq and assv are used to look up a value in an association list. They differ in which function they use to determine equality, equal? or eq? or eqv? respectively. We shall use assoc in these notes, although assq would be more efficient.


(example '(assoc 'x '((a . 3) (x . 2))) '(x . 2))

Note that we are using "dotted pairs" here, rather than the proper lists that we used previously with assoc. This is a more space-efficient choice.

3.2 A Scheme Interpreter: eval_eager is the eager, substituting version

Let us write a function eval_eager which will evaluate a Scheme expression. Now an expression can be either a pair or not.

If the expression is a pair [1] it means that we have a function applied to arguments, or a special form. We call the function eval_compound [2] to handle this case.

If the expression is not a pair [3] then it always evaluates to itself.


(define (eval_eager expr)
    (if (pair? expr)                                  ; [1]
        (eval_compound (car expr) (cdr expr))         ; [2]
        expr )                                        ; [3]
    )

(example '(eval_eager 34) 34)

Notice that for an unbound variable x we have:


(example '(eval_eager 'x) 'x)

This makes our evaluator behave in a way closer to how one would like a symbolic algebra system (for example Mathematica) to behave.

3.3 eval_compound evaluates special forms, and any application

The function eval_compound treats special forms by examining an association list to see if a particular function is a special form. We can call this process finding a "method" to deal with the special form. If there is no method, then we do not have a special form, so we evaluate the arguments and call apply_eager to apply the function to it.

... Finding the method for a special form

We use a global association list, alist_method, which we will define later, to map from the name of a special form to a method for implementing it. The


(define (method f)
    (let ((pair (assoc f alist_method)))
        (if pair (cdr pair) pair )
        )
    )

... The definition of eval_compound using method-lookup.

The function eval_compound examines the function of the original expression. It gets the method for the function [1] if there is one, and uses it [2] to compute the value of the expression.

Otherwise [3] we don't have a special form, so we have an actual function applied to arguments. We make a list of the evaluated arguments [4], to which the function is applied [3].


(define (eval_compound f args)
    (let ((m (method f)))               ; [1] get method if there is one
        (if m
            (m args)                    ; [2] use method if there is one
            (apply_eager f              ; [3]
                (map eval_eager args))  ; [4] otherwise evaluate the arguments
                                        ; and apply the function to them
            )                           ; end if
        )                               ; end let
    )

We will need to define at least 4 special forms to get any computation done at all. Firstly we will need one for lambda, since (lambda (args) ...) certainly does not mean "apply lambda to ....". Secondly we will need if to be a special form, since it must be evaluated lazily. Thirdly we will define a special form for Y, since it should be evaluated lazily. And finally we need to define a special form for quote. Recall that 'expr is just a shorthand for (quote expr).

... Defining the method to evaluate the special form lambda

In the substitution model, a lambda expression evaluates to itself, so we define the following method which simply reconstitutes the original expression.


(define (method_lambda args)
    (cons 'lambda args)
    )

We only actually do anything with a lambda expression when it is applied to arguments.

... Defining the method to evaluate the special form if

To evaluate a conditional expression of the form (if bool expr1 expr2) we first evaluate bool [1]. If it evaluates to #t then we evaluate expr1 [2]. If bool evaluates to #f then we evaluate expr2 [3]. Otherwise we arrange that the (if...) expression evaluates to itself. Note that here again we have a rule for evaluation that differs from standard Scheme.


(define (method_if args)
    (let ((bool (eval_eager (car args))))                   ;[1]
        (cond                                               ;
            ((eq? bool #t) (eval_eager (cadr args)))        ;[2]
            ((eq? bool #f) (eval_eager (caddr args)))       ;[3]
            (else (cons 'if args))                          ;[4]
            )
        )
    )

... Defining the method to evaluate the Y combinator.

The Y-combinator also needs to be evaluated lazily, since we could use the rule (Y e) ==> (e (Y e)) ad nauseam, that is until we run out of space. So we leave (Y e) unevaluated. Let us choose the symbol 'Y- for the name of the Y-combinator in Scheme - we do this for the obvious reason that the symbol 'y [which is the same as the sybol 'Y in Scheme] is rather too commonly used as the name of an ordinary variable.


(define (method_Y- args)
    (cons 'Y- args)
    )

Again, as with lambda, we will actually do something with Y- when we find an expression of the form ((Y e1) e2), during our implementation of apply_eager.

... Defining the method for quoted constants

Treating quote poses us a problem - we might think that (quote (1 2 3)) should evaluate to the list (1 2 3). Indeed this is fine for the non-substitution model of interpretation which we discuss later. But it is wrong for the substitution model, since we want (quote (+ 3 4)) to behave differently from (+ 3 4). So we make a quoted expression evaluate to itself.


(define (method_quote args)
    (cons 'quote args)
)

Now we can define our association list for methods:


(define alist_method
    (list
        (cons 'lambda method_lambda)
        (cons 'Y-     method_Y-)
        (cons 'if     method_if)
        (cons 'quote  method_quote)
        )
    )

... Testing out eval_eager

We can now try out those cases of the use of eval_eager which don't require us to apply a function to arguments.


(example '(eval_eager '(if #t 3 4)) 3)
(example '(eval_eager '(if #f 3 4)) 4)
(example '(eval_eager '(lambda (x) (+ x 5))) '(lambda (x) (+ x 5)))
(example '(eval_eager '(Y- (lambda (x) (+ x 5)))) '(Y- (lambda (x) (+ x 5))))
(example '(eval_eager ''x) ''x)
(example '(eval_eager ''(1 2 3)) ''(1 2 3))

3.4 Defining apply_eager to implement the application of a function.

Now we come to the definition of apply_eager. This has to deal with the primitive operations of the language, and with the case of application of lambda expressions and of expressions of the form (Y e).

... Applying primitives such as +, *, car, cons, cdr

First, we need to define some functions to handle primitives, before we attempt apply_eager. Applying a primitive is quite easy - we just use the built-in function apply.


(define (apply_prm f args) (apply f args))

However we have to treat list-manipulating primitives specially - essentially we strip off quotes from arguments, apply the built-in Scheme function, and replace quotes as required to ensure that any constant that might be mistaken for an expression is suitably quoted.

The apply_prm_q function strips off any quote, calls f, which will be a list-processing function like car and puts quotes back on if the result is a pair (and so might be confused with an expression).


(define (apply_prm_q f l)           ; f will be car, cdr ...
     (requote (apply f (map de_quote l)))
    )

The requote function puts quotations back on to lists and symbols - those things which must be quoted for they will otherwise be mistaken for expressions.


(define (requote x)
   (if (or (pair? x) (null? x) (symbol? x)) (list 'quote x) x)
)

The de_quote function takes off the quotes.


(define (de_quote l)
   (if (and (pair? l) (eq? (car l) 'quote)) (cadr l) l)
)

... The implementation of apply_eager

What has to happen in the call (apply_eager f args) is [1] if f is a symbol which names a primitive operation, we call apply_prm to provide a value. In the case [2] of list-processing primitives, we call apply_prm_q to strip off quotes where necessary, and restore them as required.

If [3] f is a symbol which does not name a primitive, we evaluate (f args) to itself. Note that it is not possible that f should be a variable whose value is a user-defined function, because in the substitution-based model of evaluation, such variables are substituted for by the function they denote before apply_prm gets its hands on them. We return (f args) as a way of supporting symbolic evaluation. Scheme, in the equivalent situation, reports an error.

If [4] the function f is a pair, we call apply_compound to work out what the value should be. Any other value [5] of f is illegal.


(define (apply_eager f args)
    (cond
        ((symbol? f)
         (cond
             ((eq? f '+)     (apply_prm + args))       ; [1] apply primitives
             ((eq? f '-)     (apply_prm - args))
             ((eq? f '*)     (apply_prm * args))
             ((eq? f '/)     (apply_prm / args))
             ((eq? f 'null?) (apply_prm_q null? args)) ; [2]
             ((eq? f 'car)   (apply_prm_q car args))
             ((eq? f 'cdr)   (apply_prm_q cdr args))
             ((eq? f 'cons)  (apply_prm_q cons args))
             (else (cons f args))                      ; [3]
             )                                         ; end primitives
         )
        ((pair? f)
         (apply_compound f (car f) (cdr f) args)       ; [4]
         ; end cond
         )                                             ; end (pair? f)
        (else  (error                                  ; [5]
                "illegal object applied to arguments"
                f args))
        )
    )

... Examples of the use of apply_eager

We can now try out apply_eager in those cases in which we have a primitive applied to arguments, or [1], when we have a symbol that denotes no primitive applied to arguments.


(example '(apply_eager '+ '(2 3)) 5)
(example '(apply_eager '* '(2 3)) 6)
(example '(apply_eager 'car '('(2 3))) 2)
(example '(apply_eager 'cdr '('(2 3))) ''(3))
(example '(apply_eager 'cdr '('(3))) ''())
(example '(apply_eager 'cons '(2 3)) ''(2 . 3))
(example '(apply_eager 'null? '('(2 3))) #f)
(example '(apply_eager 'f '(2 3)) '(f 2 3))       ;[1]

3.5 Implementing the application of a compound function to arguments

Now we turn to the call (apply_compound f fn_f rest_f args). Here f is the original function that was passed to apply_eager, for example

        (lambda (x y) (+ x (* 2 y)))

while fn_f is whatever is the first element of the list which is f - in this example it will be lambda. The variable rest_f holds the remainder of f, in the example if will be ((x y) (+ x (* 2 y))), while the variable args holds the argument(s) that f was applied to. Thus if we started off by trying to evaluate.

    ((lambda (x y) (+ x (* 2 y))) 3 (+ 7 5))

args will be the list (3 12).

There are three cases here, 2 special and 1 general:

So, let us now write apply_compound, to determine which of the above 3 cases we have and call an appropriate function for each case.


(define (apply_compound  f fn_f rest_f args)
    (cond
        ((eq? fn_f 'lambda)
         (apply_lambda                             ; case [1] above
             (car rest_f)                          ; formal parameters
             args                                  ; actual parameters
             (cdr rest_f)                          ; body of lambda-expr
             ))

        ((eq? fn_f 'Y-)                            ; Y-combinator?
         (apply_y f rest_f args))                  ; case [2] above

        (else  (apply_eager (eval_eager f) args)   ; case [3] above
            )
        )
    )

This leaves us needing to write apply_lambda [1] and apply_y [2] to define.

... Implementing the application of a function of the form (Y- e)

Case [2] Let us begin with treating apply_y, which is simpler. What we need to do is convert ((Y- e) args) into

    ((e (Y- e)) args)

Since e will normally be an ordinary lambda-expression, it will be applied in the ordinary way.


(define msg_Y   "Y-combinator takes one argument")

(define (apply_y ye list_e args)
    (if (= (length list_e) 1)               ; check we don't have (Y- e1 e2..)
        (let ((e (car list_e)))             ; get e
            (eval_eager
                (cons (list e ye) args)     ; form (e (Y- e))
                )
            )                               ; end let.. => (e (Y- e))
        (error msg_Y args_y args)           ; wrong number of arguments
        )  ; end if
    )

... Implementing the application of a lambda expression

Now let us treat case [1] above, that is the application of a lambda expression applied to arguments. The call (apply_lambda vars args body) will apply the lambda expression (lambda vars body) to the list of arguments args. For example if we are evaluating the expression:

    ((lambda (x y)  (+ x (* 2 y))) 3 (+ 5 7))

then vars will be the list (x y), args will be the list (3 12) and body will be ( (+ x (* 2 y))). Note that Scheme syntax allows body to be a list of expressions. We assume that there is only one expression in body, since we are restricting ourselves to a functional model of computing.

We can now write apply_lambda. If [1] body is a list consisting of a single expression, we call eval_eager [2] to evaluate what we get when we substitute [3] actual parameters for formal parameters in the expression. The function multi_subst performs the substitution. It is given as argument an association list [4] made by using the system function map, which is like maplist except that it can map multiple lists.

If [6] body does not contain a single expression, we report an error.


(define (apply_lambda vars args body)
    (if (= (length body) 1)                                       ; [1]
        (eval_eager                                               ; [2]
            (multi_subst                                          ; [3]
                (map cons vars args)                              ; [4]
                (car body)))                                      ; [5]
        (error "lambda expression with multi-expr body" args body); [6]
        )
    )

... Implementing the substitution of values for variables

Next comes the definition of multi_subst. The call (multi_subst alist expr) will take an association list alist and an expression expr and replace each symbol in the expression which has an entry in alist by its value as specified in alist. When we are evaluating:

    ((lambda (x y)  (+ x (* 2 y))) 3 (+ 5 7))

expr will be (+ x (* 2 y)) while alist will be ((x . 5) (y . 7))

We have to take some care with lambda expressions nested inside expr. Suppose we are evaluating:

    ((lambda (x) (lambda (x) (+ x 2) (+ 3 x))) 7)

then we must not substitute 7 for x in the lambda-expression

    (lambda (x) (+ x 2))

this is implementing the scope rules for variables bound in lambda- expressions.

So here is the definition of multi_subst. If [1] we have a symbol, we look up its value in alist, returning the value if there is one, otherwise we return the symbol unchanged.

Otherwise, if we have a pair [2] we look to see if we have a lambda expression [3], in which case we call the helper function multi_subst_lambda. Otherwise [4] we proceed by deep recursion, rebuilding the substituted car and cdr of the expression.

Finally [5] if expr is neither a symbol nor a pair it must be a constant, which is unchanged.


(define (multi_subst alist expr)
    (cond
        ( (symbol? expr)                          ; [1]
         (let ((pair (assoc expr alist)))         ; value is in alist
             (if pair (cdr pair) expr)            ; use it if there is one.
             )
         )                                        ; end symbol?
        ( (pair? expr)                            ; [2] pair?
         (if (eq? (car expr) 'lambda)             ; [3] lambda? - use helper
             (multi_subst_lambda
                 alist (cadr expr) (cddr expr))
             (cons (multi_subst alist (car expr)) ; [4] recurse deeply
                 (multi_subst alist (cdr expr)))
             )
         )
        (else expr)                              ; [5] must be constant
        )                                        ; end cond
    )

Warning the multi_subst function defined above is still, in its handling of free variables, short of fulfilling the requirements of substitution in the lambda calculus. The actual definition of substitution is to be found in page 6 of the notes on the lambda calculus on the 287 menu. However, the above definition of multi_subst willl fill our needs for this class.


(example '(multi_subst '((x . 7) (y . 4)) '((+ y (* 5 x))))
          '((+ 4 (* 5 7))))

So we can now apply lambda expressions:


(example '(apply_eager '(lambda (x) (* 5 x)) '(7)  )   35 )

And, moreover, we can evaluate expressions involving them:


(example '(eval_eager '((lambda (x) (* 5 x)) 7)  )   35 )
(example '(eval_eager '((lambda (x y ) (+ y (* 5 x))) 7 4)  )   39 )

... Making a substitution in a lambda-expression

However we now have to treat substitution in a lambda-expression. To do this we must treat the variables introduced by the lambda-expression as -new- variables, so they will be deleted from the alist if they occur.

For example, if we want to perform the substitution ((a . 2) (x . 3)) in the lambda-expression (lambda (x) (+ a x)) we must substitute for a but not for x. The way we meet this requirement is [3] to delete the variables bound in the lambda-expression from the association list before we substitute in the body of the lambda-expression. The result [1] recreates the lambda-expression out of the substituted body.


(define (multi_subst_lambda alist vars body)
    (list 'lambda vars                      ; [1]
        (multi_subst                        ; [2]
            (delete_alist vars alist)       ; [3]
            (car body))
        )
    )

The function to delete variables from an association list can be defined as follows:


(define (delete_alist vars alist)
    (if (null? alist) '()
        (if (memq (caar alist) vars)
            (delete_alist vars (cdr alist))
            (cons (car alist) (delete_alist vars (cdr alist)))
            )
        )
    )

The following example illustrates the use of delete_alist.


(example
    '(delete_alist '(x y z) '((x . 2 ) (a . 4) (y . 3) (p . 7)))
    '((a . 4) (p . 7))
    )

We can now check out substitution


(example
    '(multi_subst
        '((x . 7) (y . 4))
        '((lambda (x) (+ x y))  (* 5 x))
        )

    '((lambda (x) (+ x 4))  (* 5 7))
    )

And we see that the variable x bound in the lambda expression is not substituted for in the body of the lambda expression expression but is substituted for elsewhere, while the variable y is substituted for in the body of the lambda expression.

We can see how this mechanism supports higher order functions:


(example
    '(eval_eager
        '((lambda (f)
             (lambda (x)
                 (f (f x))) )
         car))
    '(lambda (x) (car (car x)))
    )

Finally let us try a simple use of the Y-combinator to find the sum of an empty list.


(example
    '(eval_eager
        '((Y- (lambda (sum)
                 (lambda (l)
                     (if
                         (null? l)
                         0
                         (+ (car l) (sum (cdr l))))
                     )
                 )        ;end lambda
             )            ; end (Y- E)
              '()))
    0)

and a non-empty list:


(example
    '(eval_eager
        '((Y- (lambda (sum)
                 (lambda (l)
                     (if
                         (null? l)
                         0
                         (+ (car l) (sum (cdr l))))
                     )
                 )
             )  '(2 3 4)))
    9)

If you want to see this computation performed in mind-numbing detail, try typing

    (trace eval_eager)

and running the last example again.

4 Shonfinkel's Combinators enable us to dispense with lambda

This section is optional

Mathematically the lambda calculus is a little tricky to reason about because of the concept of free and bound variables. There is an alternative formulation, using concepts developed by Shonfinkel in the 1920's which requires no notion of variable. However, this work is best approached by using mathematical conventions for understanding functions in which a function only ever has one argument. With these conventions we can avoid a lot of parentheses with the convention that f x means "apply f to x" and f x y means "apply f to x and then apply the resulting function to y, that is ((f x) y). Also, using something closer to mathematical conventions we shall write (lambda x. expr) for (lambda (x) expr).

The approach to functional programming based on Shonfinkel's work makes use of the combinators, S,K and I, characterised by the following reduction rules:

[S1]    S f g x => f x (g x)
[S2]    K x y   => x
[S3]    I x     => x

Any lambda-expression can be translated into S,K and I (the good news), but at a quadratic penalty in time and space (the bad news). This translation takes place by applying the rules given below until no lambda-expressions are left.

[T1]    (lambda x. x)       ->  I
[T2]    (lambda x. c)       ->  K c             [where c/=x,
                                                 c is not an application]
[T3]    (lambda x. e1 e2) -> S (lambda x. e1)  (lambda x. e2)

So consider the expression

     (lambda x. + x x) 5

This is, putting in the parentheses to emphasise the application structure

     (lambda x. (+ x) x) 5

Applying the S-translation T3 we get:

  ->   S (lambda x. (+ x)) (lambda x. x) 5

Applying T1 we get -> S (lambda x. (+ x)) I 5

Applying T3

  ->    S (S (lambda x. +) (lambda x. x)) I 5

and, from T1 and T2

  ->    S (S  (K +) I) I 5

We can now use the reduction rules to evaluate:

     S (S  (K +) I) I 5

  => S (K +) I 5 (I 5)

  => K + 5 (I 5) (I 5)

  => + (I 5 ) (I 5)

  => + 5 (I 5)

  => + 5 5

  => 10

It is clear that we could modify our implementation of eval_eager to use these combinators, and that the resulting program would be simpler, since we would replace the complicated function apply_lambda by code very similar to that which we used for the Y- combinator - essentially code to check whether we had an application involving S, I or K, and if so to implement the appropriate rule [S1], [S2] or [S3]. Moreover, we would easily get it right, which we didn't quite do for lambda (see the cautionary warning in the section on substitution).

One snag is that, as noted aboove, that a lambda-expression translates into a combinatorial expression of size quadratic in the size of the original lambda-expression. This snag can be addressed by introducing additional combinators which support a more compact encoding. A discussion of this possibility is to be found in Simon Peyton Jones's book "The Implementation of Functional Programming Languages", published by Prentice Hall (ISBN 0-13-453333-X or in paperback 0-13-453325).

Experimental computers designed to implement the combinatorial approach have been built. However they have not proved competitive, even for supporting functional languages, essentially because they do not exploit the very efficient mapping from variable to value that can be implemented using conventional machine memory.