Harry R. Schwartz

Code writer, sometime Internet enthusiast, attractive nuisance.

The author at the Palais du Luxembourg in Paris, November 2022. hacker news gitlab sourcehut pinboard librarything 1B41 8F2C 23DE DD9C 807E A74F 841B 3DAE 25AE 721B

Vancouver

British Columbia

Canada

vegan


Building an Object System from Closures

hrs

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.

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 and case 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 parse args.
  • 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 the let 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: