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.
(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:
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))))
Exercise
- Provide implementations of
car and cdr
such that:
(car (cons a b)) = a
(cdr (cons a b)) = b