Java's try-with-resources in Lisp

There is an HN thread about the new try-with-resources feature in "build 105, the compiler and runtime of Java 7 Releases" of Java that implements some automatic resource management for opening file/stream handles, and subsequent closing and cleanup.

Of course, there wouldn't but a fuss around this if the language had macros and allowed the programmer explicit control over non-local exits (i.e. defmacro and unwind-protect.) Here is how a Lisper might have done this, all by themselves without having to reach out to Oracle for a fix, and of course, there wouldn't be a press release around it.

We would need a name for the new construct; Oracle calls it Automatic Resource Management (ARM), we will call it WITH-OPEN.

We need the WITH-OPEN macro, which is a binding form modeled after LET and the various WITH-* macros in Lisp. WITH-OPEN takes a list of arguments, consisting of (variable initform) pairs, followed by a body for forms to execute, within which the bound variables are visible. Initform is an initialization form, consiting of a pair of type and path; where type specifies the type of the resource to open, and path is a generic file pathname, URI, or any resource pointer that allows us to reach the resource in anyway apparent to the construct user (but not the implementor.)

(with-open ((var1 (:type1  path1))
            (var2 (:type2  path2))
  .. body ..)

A good, if convoluted, example might be the following:

(with-open ((file (:file #p"/usr/share/dict.txt"))
            (japan-timezone  (:url "http://timezones.com/current-time.php?tz=GMT+9"))
            (fd (:socket "dev.example.com" :port 8080))
            (plotter (:device "/dev/plotter")))
  (foo file)
  (frop japan-timezone)
  (quux fd)
  (plot-line plotter 0 0 25 80))

In this scenario, we have four resources that are different, but also share a few commonalities. And to be sure, there are far more types, and uses, of resources that we don't know of or even exist yet. That's why we will design our WITH-OPEN macro to be user extensible. The initializition form, initform, has the type of the resource as a first argument, you will see why now.

As you can see, the initforms have variable length. Three of them have initforms as (:type "path") but the fd takes an extra port argument. So can other initforms, as many needed by the underlying resource management functions. E.g:

(with-open ((file (:file #p"/usr/share/dict.txt" :if-does-not-exist :create :external-format :utf-8))
            (japan-timezone  (:url "http://timezones.com/current-time.php?tz=GMT+9" :follow-redirects t))
            (fd (:socket "dev.example.com" 8080 :reuse-address :keep-alive :do-not-route))
            (plotter (:device "/dev/plotter" :width 200 :scale :inch :paper-size :fit-to-paper t)))
  (foo file)
  (frop japan-timezone)
  (quux fd)
  (plot-line plotter 0 0 25 80))

As you can see; the real-world is a hairy messy place and we can't assume anything is fixed about how our macro might be used. So the most convenient thing to do is let the user extend the macro as they see fit. And now, on to the macro.

WITH-OPEN consists of simple a binding form that binds user-specified variables, to certain "open" resource values. It then runs a sequence of forms in its body, within which the variables are bound, and when the body executes fully, or ends abruptly, a clean up form is executed which takes care of "closing" the resources pointed to by the variables in a certain manner the user is aware of.

(defun gen-constructors (&rest args)
  (loop for arg in args
     collecting (list (caar arg) `(open-thing ,@(cadar arg)))))

(defun gen-destructors (&rest args)
  (loop for arg in args
     collecting `(close-thing ,(caadar arg) ,(caar arg))))

(defmacro with-open ((&rest args) &body body)
  `(let ,(gen-constructors args)
        ,@(gen-destructors args)))))

The two gen-* functions are mere helpers to generate constructors and destructors. If you want to improve the reliability of the macro you need to just look at those two functions.

Within the generators, you can see us constructing a list of function calls: OPEN-THING and CLOSE-THING. They are actually methods, and below is the generic function protocol for them

(defgeneric open-thing (type uri/path &optional &key &allow-other-keys)
  (:documentation "opens anything that might be reachable via URI/PATH and returns a handle"))

(defgeneric close-thing (type handle &optional &key &allow-other-keys)
  (:documentation "closes the thing"))

This is the source of our macro's flexibility. The methods are called against each initform, successively. The first argument, TYPE, is what the methods use to dispatch on specific types. The generic-function takes two required arguments, as per the initform, but also takes optional extra arguments that each initform might need to accept from the user to do a better job. Let's extend the macro with a first implementation for :FILE types

(defmethod open-thing ((type (eql :file)) file-path &rest open-args)
  (format t "Opened thing ~a ~a~%" type file-path)
  (apply #'open file-path open-args))

(defmethod close-thing ((type (eql :file)) handle &rest close-args)
  (format t "Closed file ~a~%" handle)
  (close handle))

This implementation for :FILE types uses Common Lisp's builtin OPEN and CLOSE file operations. When you implement backends for other types of resources, you can use whatever resource specific functions there are; to preparing a resource for use, and subsequent clean up.

For each type of resource, the user can add his or her own pair of OPEN-THING and CLOSE-THING methods, that do exactly what they want, and they can pass whatever arguments they need to their methods via the WITH-OPEN initforms. The macro iteself will never need to be touched again, save for the missing error checking and stability stuff (one thing it should is collect only list bindings, i.e. bindings with both variable and initform, which is as simple as adding a "when (consp arg)" clause to the LOOPs in the generators)