let there be fewer parentheses

Lisp binding forms like let are prone to excruciating nests of parentheses: (let ((x (some expression))) ...). They can be hard to read, hard to edit, and surprisingly verbose. Can we do anything about this?

I've tried the alternating-list variation of let, which saves a pair of parentheses per variable:

(let* (x 2
       y (+ x x))
  (= y 5))

Unfortunately, that's its only virtue. Despite the reduced parentheses, it can be harder to read than regular let, because there is nothing delimiting each binding. It's also inconvenient to use in macros, because the alternating list often has to be split into names and init-forms, and mapcar won't do it. This would be easy if there were sequence functions for alternating lists, but the lack is a reminder that the alternating list is a slightly unnatural structure. Maybe we shouldn't use it. Rather than removing the pair of parentheses around each binding, we should look for ways to remove those around the list of bindings.

Every Lisp form gets an implicit list for free: its tail. (Well, not for free - it costs a pair of parentheses, but those are already spent.) Many macros use the list as an implicit progn. This is obviously useful for stateful code, and for internal define (when available). But modern Lisp style is increasingly functional, and of course define is unlikely in a let. There is usually only one body form, so the implicit list is wasted.

We can use the implicit list as the list of bindings instead, for a form reminiscent of Haskell's where syntax (but as an expression, not a feature of top-level definitions):

(defmacro where (body &rest bindings)
  "Backwards LET* - one body form followed by zero or more bindings."
  `(let* ,bindings ,body))

(where (= y 5)
  (x 2)
  (y (+ x x)))

This puts the body first, which is often a natural order. Unfortunately it may be confusing in an eager language, since forms no longer appear in order of evaluation.

Update May 2012: Mitchell Wand proposed this in 1993.

It's possible to put the body-form last, and use the implicit list at the beginning. This is an unusual pattern, but not hard to implement:

(defmacro lets (&rest bindings-and-body)
  "LET* with fewer parentheses and only one body form."
  (if bindings-and-body
    (let ((body (car (last bindings-and-body)))
          (bindings (butlast bindings-and-body 1)))
      `(let* ,bindings ,body))

(lets (x 2)
      (y (+ x x))
  (= y 5))

Just don't expect your editor to indent it properly. :)

There is something unsatisfying about all of these solutions, because the problem they solve is purely one of syntax. The problem with let isn't that it has a list of binding pairs - that's its natural structure. The problem is that we have to explicitly delimit that list and those pairs, and we use the same characters for both. Languages with more syntax can avoid both of these problems.

Alternatively, you can avoid let by using internal define instead. I have a post on that coming soon.

No comments:

Post a Comment

It's OK to comment on old posts.