Closures: The Poor Man’s Object

Common Lisp comes with a really robust OOP system, CLOS. A good deal of Common Lisp itself is made with CLOS, in fact. But objects aren’t mandatory in Lisp, and you can get quite far without CLOS if you really want to avoid overhead, by using closures.

Closures in Lisp are, essentially, lambdas defined within their own lexical scopes. [1] An example would be to define a lexical scope with the symbol ‘x bound to 3, and to create a function that returns x.

CL-USER> (defparameter my-closure 
           (let ((x 3))
             (lambda () x)))

MY-CLOSURE

CL-USER> (funcall my-closure)
3

CL-USER> (boundp 'x)
NIL

We’ve defined ‘my-closure as a var containing a lambda, that lambda was defined in a custom lexical scope where ‘x is bound to 3. We can call #’my-closure and it’ll return what it sees as x: 3. Though if we check whether ‘x is bound in the global scope, we see it isn’t.

This might seem a bit inconsequential, but from this little consequence we can make objects with private data.

(defun make-person (&optional (person-name "Anon"))
  (let ((private-name person-name))
    (list :getter (lambda () private-name)
          :setter (lambda (new-name) (setf private-name new-name))
          :char-count (lambda () (length private-name)))))

Here I define a function that returns a list of three functions: a getter, setter, and char-count function, which are all defined within a dynamically created lexical scope unique to those returned functions.

I define ‘my-person as the return of #’make-person, we now have a list of three closures.

CL-USER> (defparameter my-person (make-person))
MY-PERSON

CL-USER> my-person
(:GETTER #<CLOSURE (LAMBDA () :IN MAKE-PERSON) {100252368B}> 
 :SETTER #<CLOSURE (LAMBDA (NEW-NAME) :IN MAKE-PERSON) {10025236AB}> 
 :CHAR-COUNT #<CLOSURE (LAMBDA () :IN MAKE-PERSON) {10025236CB}>)

We have an initial value for the person’s name; “Anon”. If we call the getter function in ‘my-person, we should get “Anon”, and if we call the setter, passing a new name, we should get the new name from the getter thereafter.

CL-USER> (funcall (getf my-person :getter))
"Anon"

CL-USER> (funcall (getf my-person :setter) "Gerald")
"Gerald"

CL-USER> (funcall (getf my-person :getter))
"Gerald"

It’s not limited to just getters and setters. I defined a simple char-count function too, that returns the n of chars in the person’s name.

CL-USER> (funcall (getf my-person :char-count))
6

‘person-name is, of course, only bound within the scope of ‘my-person, and so we can create more person-closures who all internally refer to ‘person-name without any collisions.

CL-USER> (defparameter my-other-person (make-person "Geoff"))
MY-OTHER-PERSON
CL-USER> (defparameter my-third-person (make-person "Paul"))
MY-THIRD-PERSON

CL-USER> (funcall (getf my-person :getter))
"Gerald"
CL-USER> (funcall (getf my-other-person :getter))
"Geoff"
CL-USER> (funcall (getf my-third-person :getter))
"Paul"

And just like that, we have objects.

Notes

  1. Closures, the noun, and closure, the abstract-noun, are different things. Where as closures are what we discussed here, the abstract notion of closure is when functions are closed under a set of operations. Integers for example are closed under the operation of addition, you can add any integer to any other integer and an integer is returned, which can in turn be added to other integers. Closure is when the outputs of functions can be used as inputs to those same functions.

Leave a Reply

Your email address will not be published. Required fields are marked *