Building an Object System from Closures
Published . Tags: computer-science, lisp.
When you’re first learning about functional programming, you sometimes hear that objects are equivalent to closures. What the hell does that mean?
Let’s start with definitions: an object is a thing with behavior and state. An object’s behavior is just its public interface; it’s the set of messages that the object can receive. An object’s state consists of zero or more internal variables that methods on the object can manipulate. In other words, an object’s behavior is defined by its methods and its state is defined by its instance variables. So far, so good.
If you’ve got a simple functional programming language, you can model these concepts really neatly! An object’s behavior can be defined by a function that accepts messages and dispatches them, and its state is a closure over that function. Using this model, a constructor is just a function that returns such a closure.
That means that if you’ve got a language with closures and first-class functions (which is darn near all of them these days) then you can build your own object system.
Let’s get a little more concrete and take a look at an example (using Scheme as our implementation language):
(define (make-counter) (let ((count 0)) (lambda (message) (case message ('get count) ('increment (set! count (+ 1 count))) ('reset (set! count 0)) (else (error "Unknown method:" message))))))
This is a “constructor” called
make-counter that returns a function. We can
call that function and pass it a symbol (that is,
message) which determines
which “method” should be called.
case expression dispatches on the
message to determine the function’s
behavior (we can call this kind of expression a dispatch table). That behavior
might include manipulating
We can use it like so:
(define counter (make-counter)) (counter 'get) ; => 0 (counter 'increment) (counter 'increment) (counter 'increment) (counter 'get) ; => 3 (counter 'reset) (counter 'get) ; => 0 (counter 'undefined-behavior) ; ERROR! "Unknown method: undefined-behavior"
Cool! It looks like
counter is responding to messages and managing internal
state, which sounds an awful lot like an object to me. Mission accomplished!
This example was intentionally simple, but there’s plenty that we could do to extend it:
- We can add more state and more behavior by adding items to the
- None of our messages currently accept arguments. We could extend the anonymous
function to take an optional argument list (with syntax like
(lambda (message . args) ... )) and modify the appropriate elements in the dispatch table to parse
- Inheritance? Not too difficult! We’ve got a few options here, but they all
revolve around modifying the
elsecase in the dispatch table to delegate messages to a “parent” object. We could create this parent object in the
letstatement and treat it like any other piece of internal state.
- To extend the previous point, we could also go crazy and pass the parent
object in as an argument to the constructor, which lets us assign an object’s
parent on a case-by-case basis: need a
'carthat inherits from
(make-car (make-elephant)). Boom: we just built an elephant car.
- Our objects don’t currently provide any introspection, so we can’t tell what
messages they respond to. We could provide that by making the dispatch table
into its own object, which could
'registernew behaviors and test whether it
'responds-to?a given message. Our objects could delegate their own
'responds-to?calls to their respective dispatch table.