Building an Object System from Closures
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.
The 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 count
.
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
let
andcase
statements, respectively. - 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 parseargs
. - Inheritance? Not too difficult! We’ve got a few options here, but they all
revolve around modifying the
else
case in the dispatch table to delegate messages to a “parent” object. We could create this parent object in thelet
statement 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
'car
that inherits from'elephant
? Easy:(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
'register
new behaviors and test whether it'responds-to?
a given message. Our objects could delegate their own'responds-to?
calls to their respective dispatch table.
You might like these textually similar articles: