Advanced Methods in Programming Languages (201-2-4411-01)
HW Assignment 3 - Spring 2003 - Michael Elhadad

Lambda Calculus - Unification - Simple Interpreter

Problems for Mon 24 Mar 2003

Syntax of the Lambda Calculus

The following is an s-expr based concrete syntax for the lambda calculus with multiple parameters. Refer to it in the following questions:
<exp>   ::= <symbol>                        varref (var)
               |  (if <exp> <exp> <exp>)    if-exp (test-exp then-exp else-exp)
               |  (lambda ({<var>}*) <exp>) lambda-exp (formals body)
               |  (<exp> {<exp>}*)          app-exp (rator rands)
  1. Stack ADT
  2. Terms
  3. Substitution
  4. Unification
  5. Reduce-once for the multi-parameters lambda calculus
  6. Reduce-all

Stack ADT

[From the textbook, Exercise 2.14 p.56 and 2.19.] Consider the data type of stacks of values, with an interface consisting of the procedures empty-stack, push, pop, top and empty-stack?. Write a specification for these operations in the style of the finite-function example. Which operations are constructors and which are accessors? Implement the stack data type using an abstract syntax tree representation. This is an example of an Abstract Data Type (ADT) with several accessors. In the procedural representation for such ADTs, one must design one procedure for each accessor.

Terms

[From the textbook, Exercise 2.13 p.54.] Define a term to be either a variable, a constant (either a string, a number, a boolean, or the empty list), or a list of terms. We can use the following data type to define the abstract syntax of terms.
(define-datatype term term?
  (var-term (id symbol?))
  (constant-term (datum constant?))
  (app-term (terms (list-of term?))))
We represent a term using symbols for variables and lists for app terms, while treating everything else as a constant. Thus the term:
("append" ("cons" w x) y ("cons" w z))
represents an abstract syntax tree that can be built by:
(app-term
  (list
    (constant-term "append")
    (app-term
      (list
        (constant-term "cons") (var-term 'w) (var-term 'x)))
    (var-term 'y)
    (app-term
      (list
        (constant-term "cons") (var-term 'w) (var-term 'z)))))
Implement parse-term, unparse-term and all-ids for this term language. all-ids finds all the variables in a term.

Substitution

[From the textbook, Exercise 2.24 p.64] Define a substitution to be a function whose domain is the set of Scheme symbols and whose range is the set of all terms (as defined above). The interface for substitutions consists of:
  1. (empty-subst) binds its argument to a variable term of its argument referred to as a trivial association. (That is, call the empty susbstitution e, then (e x) --> (var-term x) for all symbol x.)
  2. (apply-subst s i) returns the value of substitution i in substitution s.
  3. (extend-subst i t s) returns a new sustitution like s, except that symbol i is associated with term t.
Implement the data type of substitutions with both a procedural representation and an abstract syntax tree representation. Then implement a procedure subst-in-term that takes a term and a substitution and walks through the term replacing each variable with its association in the substitution. Finally, implement a procedure subst-in-terms that takes a list of terms as parameter.
;; Sample applications
(unparse-term
  (subst-in-term
    (parse-term '("append" ("cons" w x) y ("cons" w z)))
    (extend-subst 'w (parse-term '("cons" a b))
      (extend-subst 'y (parse-term '("cons" b c))
        (empty-subst)))))

-->

("append" ("cons" ("cons" a b) x) ("cons" b c) ("cons" ("cons" a b) z))  


(unparse-term
  (subst-in-term
    (parse-term '("cons" w x))
    (extend-subst 'w (parse-term '("cons" a b))
      (extend-subst 'a (parse-term '("cons" b c))
        (empty-subst)))))

-->

("cons" ("cons" a b) x)

Unification

[From the textbook, Exercise 2.25 p.64] An important use of substitutions is in the unification problem. The unification problem is: given two terms t and u, can they be made equal? More precisely, is there a substitution s such that (subst-in-term t s) and (subst-in-term u s) are equal? We say that such an s unifies t and u. There may be many such unifiers, but there will always be one that is the most general. The code below shows part of an algorithm to find the most general unifying substitution. If no such unifier exists, it returns #f.
(define unify-term
  (lambda (t u)
    (cases term t
      (var-term (tid)
        (if (or (var-term? u) (not (memv tid (all-ids u))))
            (unit-subst tid u)
            #f))
      (else
        (cases term u
          (var-term (uid) (unify-term u t))
          (constant-term (udatum)
            (cases term t
              (constant-term (tdatum)
                (if (equal? tdatum udatum) (empty-subst) #f))
              (else #f)))
          (app-term (us)
            (cases term t
              (app-term (ts) (unify-terms ts us))
              (else #f))))))))

(define unify-terms
  (lambda (ts us)
    (cond
      ((and (null? ts) (null? us)) (empty-subst))
      ((or (null? ts) (null? us)) #f)
      (else
        (let ((subst-car (unify-term (car ts) (car us))))
          (if (not subst-car)
              #f
              (let ((new-ts (subst-in-terms (cdr ts) subst-car))
                    (new-us (subst-in-terms (cdr us) subst-car)))
                (let ((subst-cdr (unify-terms new-ts new-us)))
                  (if (not subst-cdr)
                      #f
                      (compose-substs subst-car subst-cdr))))))))))
Complete the algorithm by extending the substitution interface with the two procedures unit-subst and compose-substs. The application (unit-subst i t) returns a substitution that replaces symbol i with term t and replaces any other symbol by its trivial association. The application (compose-substs s1 s2) returns a substitution s3 such that for any term t, (subst-in-term t s3) returns the same term as (subst-in-term (subst-in-term t s1) s2). The memv test in unify-term is called the occurs check. Create an example to illustrate that this test is necessary.

Reduce-once for the multi-parameter lambda calculus

The following function defines the "Applicative-order strategy" used in Scheme. With this strategy, no reduction is ever applied inside the body of a lambda abstraction. This function works for the lambda-calculus syntax with one parameter only.
;; For the one-parameter syntax of lambda calculus

(define (answer? exp) (not (app? exp)))

(define (reduce-once e success fail)
  (cases exp e
    (varref (var) (fail))
    (lambda (formal body) (fail))
    (app (rator rand)
      (if (and (lambda? rator) (answer? rand))
          (success (beta-reduce exp))
          (reduce-once rator
            (lambda (reduced-rator)
              (succeed (make-app reduced-rator rand)))
            (lambda ()
              (reduce-once rand
                (lambda (reduced-rand)
                  (succeed (make-app rator reduced-rand)))
                fail)))))))
This function receives a parsed expression and two continuations. If the expression e can be reduced by beta-reduction, the function passes the reduced parsed expression to the success continuation. If more than one beta-redex node exists in the expression, this function will select the first applicable one according to the applicative order strategy. If the expression e does not contain any beta-redex node, then the fail continuation is invoked with no parameters.

Extend this function to work over the multi-parameter lambda-calculus according to the syntax provided above. Your function will determine the order of evaluation of arguments provided to a lambda function - in such a way that arguments are evaluated left to right.

;; Sample invocation
(reduce-once (parse '((lambda (x y) (x y))
                      ((lambda (x) x) a)
                      ((lambda (z) y) b)))
             (lambda (reduced-exp) (pretty-print (unparse reduced-exp)))
             (lambda () "expression cannot be reduced"))

reduce-all

Use the function reduce-once from the previous question to implement the function reduce-all. reduce-all keeps applying reductions to its parameter (an unparsed expression) until no more reduction can be found. If it returns (that is, if no infinite loop is hit), it returns the unparsed reduced expression.
;; Sample invocation
(reduce-all '((lambda (x y) (x y))
              ((lambda (x) x) a)
              ((lambda (z) y) b)))

Last modified 10 Mar 2003
Michael Elhadad