Alternating cond

The traditional cond used in most Lisps, including Scheme and CL, has (test . body) pairs:

(define (signum x)
  (cond ((< x 0) -1)
        ((> x 0) 1)
        (else 0)))

That has Lots of Irritating Superfluous Parentheses, of a sort that are particularly annoying because they aren't forms. Arc and Clojure avoid this by alternating tests and bodies:

(defn signum [x]
  (cond (< x 0) -1
        (> x 0) 1
        :else 0))

Which is nice. But: when test and body no longer fit on one line, it becomes hard to indent, because the list structure doesn't match the semantic structure. Indenting normally, according to list structure, makes the body look like another test:

(defn fibonacci [n]
  (cond (not (and (integer? n) (>= n 0)))
        (throw (java.lang.IllegalArgumentException.
                (str "fibonacci's argument must be a nonnegative integer: "
                     (pr-str n))))
        (> n 1) (+ (fibonacci (- n 2)) (fibonacci (dec n)))
        :else n))

I tend to read conds by vertically scanning the conditions, which doesn't work with this indentation. Indenting according to sense solves this, but makes the body look like a subform of the test:

(defn fibonacci [n]
  (cond (not (and (integer? n) (>= n 0)))
          (throw (java.lang.IllegalArgumentException.
                  (str "fibonacci's argument must be a nonnegative integer: "
                       (pr-str n))))
        (> n 1) (+ (fibonacci (- n 2)) (fibonacci (dec n)))
        :else n))

...and your editor will probably “fix” it anyway the next time you autoindent.

I'm tempted to rewrite with nested ifs just to avoid the misleading indentation. Which is madness, because cond is common, and tests and bodies often outgrow one line, and I shouldn't change code structure for formatting reasons. But I don't like being unable to parse my code. The main value of S-expressions is that structure is obvious, and the structure of alternating cond isn't.

The same problem can occur in any expression with alternating subforms, such as Clojure's let, or dictionaries, but in neither case is it as bad as in cond. Variable names are distinctive, and anyway tend to be short, so let is rarely formatted with initforms on their own lines. Dictionaries don't usually have large subforms, and often have distinctive :keywords as their keys, so it's harder to get the keys and values confused. cond is particularly vulnerable (probably along with friends like typecase), because it often has large subforms that look alike but don't work alike.

I think I prefer the old cond, despite the parentheses.

3 comments:

  1. Do you know about => in Scheme's cond? That's a bit of syntax that I really miss when I write Clojure.

    I think condp in Clojure supports something similar, but I never quite learned how to do it.

    The PLT/Racketeers like to use [] for grouping the test and the consequent. I don't like it personally, but it does help make the structure more explicit.

    ReplyDelete
  2. While slightly messing, using a comma to prefix the body might help you visually parse the expressions.

    ReplyDelete
  3. I hit the same problem frequently and my current solution is the following:

    (cond
    ;;---
    (= x 1)
    (do-something-thats-long)
    ;;---
    (col? x)
    (do-something-else-thats-long)
    ;;--
    :else (whatever))


    Of course you can also use real text in the comments :-)

    ReplyDelete

It's OK to comment on old posts.