COS 441 - Data Abstraction and the First Order Transformation - Feb 20, 1996

Finite Functions

A finite function is a function with finite domain:
f[d -> r](x) = { r if d = x,
                 f(x) otherwise
Let's implement finite functions in Scheme:
(define make-empty
  (lambda ()
    (lambda(x) #f)))

(define extend
  (lambda (f d r)
    (lambda (x)
      (if (eq? x d) r (f x)))))

(define lookup
  (lambda (f x)
    (f x)))
We need make-empty because we're using an inductive definition of finite functions and we need a base case.

The implementation of extend simply follows the mathematical definition above.

The reason that we use lookup rather than explicitly invoke f is to provide abstraction over the representation of finite functions. By using lookup, any client code that uses this abstraction will know nothing about the underlying implementation of finite functions as procedures. Therefore we will be free to change or improve our implementation of finite functions without making any changes to client code.

Now let's look at an alternative implementation of finite functions. But rather than pull a rabbit out of a hat, we're going to derive this new implementation from our existing one. This should give us reasonable confidence that the new implementation behaves the same way as the old one.

The First Order Transformation

We can transform the procedural implementation of finite functions to one that uses records.
(define-record empty-ff ())
(define-record extend-ff (fun dom ran))

(define make-empty make-empty-ff)
(define extend make-extend-ff)

(define lookup
  (lambda (f x)
    (variant-case f
      (empty-ff () #f)
      (extend-ff (fun dom ran) (if (eq? x dom) ran (lookup fun x))))))
To perform this transformation, we do the following: In this case, there are two lambda's that represent finite functions in the original implementation. One is returned by make-empty; the other is built by extend. Hence the two kinds of records empty-ff and extend-ff.

Moving the actions of the two lambda expressions to where they are applied means moving the lambda bodies

    #f    and     (if (eq? x d) r (f x))
to the application (f x) in lookup and to the application (f x) in the procedure built by extend. Since there are two different kinds of records that represent finite functions in the new implementation (or two different lambda's that represent finite functions in the old), we need to use variant-case at these two places.

The second application (f x) within extend might seem problematic because we need to move the procedure built by extend inside itself. To solve this quandry, we use recursion!

Other procedural representations

We can use procedural representations for many other kinds of data. For instance, we can represent pairs (i.e., cons, car, cdr) using procedures. A definition of cons follows.
(define cons
  (lambda (x y)
    (lambda (f)
      (f x y))))