Why closures are not objects with one method

Closures are generalized objects

Among the more tiresome arguments you'll see online is the question of the interchangeability of objects and closures. This has been solved for a long time now, but new generations of programmers keep ignoring it. But, as a reminder: closures are more general than objects; anything you can do with objects you can do with closures, but there are things you can do with closures you cannot do with objects.

A statement you will frequently see (thankfully usually quickly corrected) is that "closures are objects with one method". This is wrong in two important ways.

  • Closures may close over more than one function
  • One function may implement multiple methods

The first bullet point gets glossed over a lot, but it's important: even if a language limits you to a single return value, as far as I know any language that lets you pass and return a function will let you pass or return some kind of vector or tuple of functions.

The second point is also important: functions are in many cases how methods are implemented, but there need not be a one to one mapping there. One function can implement multiple methods, and one method can be implemented by multiple functions. If you think of a method as the response of an object to a message, this distinction becomes more clear: rather than closures being objects with a single method, objects are closures with a single dispatch.

The easiest way to implement multiple methods comes from that: write a function whose first argument is the message being passed, and whose remaining arguments are the method arguments (see below for an example). Obviously variadic arguments are helpful here, but not strictly necessary (you're just stuck with a lot of annoying NULL's and typecasts with fixed arity functions; this is what metaprogramming is for, anyways).

An example of how I use closures

Here is a snippet from a project I'm in the early stages of, an information theory system in Lisp:

 (defun new-alphabet ()
  (let ((alphabet nil))
    (lambda (msg obj) ; NB: single lambda, optional method argument
          (case msg
                (show alphabet)
                (add (let ((found (assoc obj alphabet :test #'equalp)))
                        (if found
                                (rplacd found (1+ (cdr found)))
                                (setq alphabet (acons obj 1 alphabet)))))
                (sum (apply #'+ (mapcar #'cdr alphabet)))
                (length (length alphabet))))))

(defun show-alphabet (alph)
  (funcall alph 'show nil))

(defun add-to-alphabet (alph obj) ; only time you actually need the obj argument
  (funcall alph 'add obj))

(defun alphabet-sum (alph)
  (funcall alph 'sum nil))

(defun alphabet-length (alph)
  (funcall alph 'length nil))

(defun frequencies (alph)
      (lambda (x) 
        (cons (car x) 
        (/ (cdr x) (alphabet-sum alph)))) 
      (show-alphabet alph)))

(defun entropy (alph &optional (base 2))
  (- (apply #'+ 
         (lambda (x) 
           (* (cdr x) 
              (log (cdr x) base))) 
            (frequencies alph)))))

Obviously, this code isn't in any shape to win awards any time soon, but it shows how incredibly simple it is to use closures to build just the parts of an object system that you want to use and ignore the rest. I could, just as easily, have made the four cases into lambda expressions and returned a list or vector of them, or (this being lisp) simply returned four values and let the caller worry about binding it (this is rude, however). Also obviously, this can use some metaprogramming to make it more convenient (and ultimately that's all defstruct does, for that matter). I'm not remotely at that point yet, but the good news is that when I get there I already have the domain boundaries set up to do whatever information hiding I want. So if I needed to change it from a single function with a message to a vector of functions, I would re-write the six "methods" (show-alphabet, add-to-alphabet, etc.) to call a function from a vector rather than pass a message. And, I stress again, a lot of this will eventually be metaprogrammed once I know it's how I want it (I can be as inefficient as I want at compile time, ultimately).

This post will obviously not make people stop being wrong on the Internet, but it's my little contribution to the non-argument.

© 2017 Weldon Goree. Home