A flatter cond

utilities — cgrand, 17 June 2011 @ 14 h 44 min

Long time, no post. I’ve had two hiatus (one of this kind and one of that kind) and I’m still going through the pile of accumulated work (including the bird book — it’s a painted snipe).

To warm up, a little macro I find quite handy:

(ns flatter.cond
  (:refer-clojure :exclude [cond]))

(defmacro cond 
  "A variation on cond which sports let bindings:
     (cond 
       (odd? a) 1
       :let [a (quot a 2)]
       (odd? a) 2
       :else 3)" 
  [& clauses]
  (when-let [[test expr & clauses] (seq clauses)]
    (if (= :let test)
      `(let ~expr (cond ~@clauses))
      `(if ~test ~expr (cond ~@clauses)))))

19 Comments »

  1. Very cool! Example in docstring contains a syntax error (presumably a typo), should be
    (cond
    (odd? a) 1
    :let [a (quot a 2)]
    (odd? a) 2
    :else 3)

    Comment by Ralph Moritz — 17 June 2011 @ 15 h 32 min
  2. @Ralph, a typo indeed. Fixed, thanks!

    Comment by cgrand — 17 June 2011 @ 18 h 12 min
  3. Very nice! I think Clojure’s for construct already has something like this built-in, right? I’d love to see this built in to Clojure’s cond.

    In any case, one of the things that has always bugged me about Lisps is the way that lets result in increased indentation. It’s all too easy to end up with the main body of lisp code indented halfway across the screen after a few lets intermingled with other necessary constructs. It is nice that let’s syntax makes it obvious what block of code the binding is valid in, but the reality is that in 99% of the use cases, we want let to take effect for the remainder of the block of code we’re in. So all we’ve done is add indentation, and add to the pile of parentheses necessary to close things off at the end of the function.

    I wonder if there’s a more general solution to this problem. For example, if you use a let with no body, e.g., (let [x 2]) then it could be automatically considered as being applicable to the end of its enclosing block.

    Racket, for example, allows definitions in the middle of any sequence of expressions, for example,
    (define (f x)
    (define y (* x x))
    (+ y y))

    Notice how the internal definition doesn’t result in any additional indentation. I think Clojure would benefit from something like this.

    Racket’s semantics for these internal defines can be a bit confusing though, and it doesn’t solve the problem that your macro solves though, so perhaps both concepts are useful and can’t be unified.

    Comment by Mark — 17 June 2011 @ 19 h 34 min
  4. @Mark, I don’t find let to be that much of a problem in Clojure because of its functional focus — and in side-effecting code the problem can be mitigated with the _ convention.

    However It’s the combination of ifs and lets that I find cumbersome at times, creating deep indentations for no goods, giving this “diagonalish” aspect to the code and that’s why I try to tackle with this small variation on cond. If you have other examples of annoyingly nesting lets in Clojure, I would be happy to have a look at them.

    Btw you are absolutely right, I borrowed this :let syntax from the for macro.

    Comment by cgrand — 18 June 2011 @ 10 h 41 min
  5. I actually find I get the most needless indentation from a series of when-lets, or when-lets mixed with regular lets. I have many functions which return nil for “no result”, and protecting against nil every time I call one of those functions results in ugly, highly indented code. Those when-lets could be rewritten with this flatter cond, but that would result in a bunch of lines like:
    :let [result (lookup 0)]
    (nil? result) nil
    :let [result2 (process result)]
    (nil? result2) nil

    which at least doesn’t have the indentation problem, but is cumbersome in its own way.

    I also have apps with a lot of recursive local functions. letfn constructs are indented weirdly by many of the editors, and when you start having lets and when-lets inside of letfn…. UGH!!!

    Comment by Mark — 18 June 2011 @ 19 h 03 min
  6. @Mark: sometimes, I wrote things like that:
    (when-let [result2 (when-let [result (lookup 0)] (process result))] …)
    but it works only when you don’t need the intermediate locals in the lexical scope of your body.

    I had dirty thoughts with < <- but I'm uneasy about using it https://gist.github.com/1033455

    I’m toying with variations if-let/when-let which support multiple binings with the semantics of and. https://github.com/cgrand/parsley/blob/4eedca88e3701c2d5bbd119c9baa772dd3d1aa09/src/net/cgrand/parsley/util.clj#L4

    I realize that this last solution wouldn’t solve the when-let lixed with regular lets problem… unless my when-let is extended to also support :let (and those bindings wouldn’t be part of the “and”.) https://gist.github.com/1033467

    Comment by cgrand — 18 June 2011 @ 22 h 36 min
  7. Hmmm, interesting idea; multiple bindings for when-let makes a lot of sense, but I’m not sure I like the look of the :let statements within to handle the mixture.

    Maybe another option is to incorporate :when-let into your flatter cond macro, e.g,
    (cond
    :when-let [x (lookup y)]
    :let [z (f x)]
    (even? z) blah

    Optionally, make the :when-let support multiple bindings, and I think you’ve got a pretty nice DSL for handling all the common mixtures that cause indentation issues.

    Comment by Mark — 19 June 2011 @ 4 h 55 min
  8. @Mark re: :let inside when-let, I’m not fond of them either.

    There’s still something bugging me about your :when-let proposal, maybe it’s just the name of the keyword or I simply need to get used to it.

    A more rational issue that your :when-let doesn’t address is the case of nested if-lets where all the else forms have the same value and that the eventual then form may return nil. I quickly thought about adding an :if-let that would short circuit to :else (or implicit else) but it seems far fetched — or at least not fluent using the current choice of keywords. (edit: I realize that this case is covered by if-let+:let — for combining if-let with cond see my next comment).

    Any idea?

    Comment by cgrand — 19 June 2011 @ 12 h 38 min
  9. Another idea I had: if a test expression in a cond is a vector it should be considered as a binding for an if-let. That is:

    (cond
    [[x & xs] (seq coll)] 1
    [[x & xs] (seq coll)] 2)

    would be equivalent to

    (if-let [[x & xs] (seq coll)]
    1
    (if-let [[x & xs] (seq coll)]
    2))

    Comment by cgrand — 19 June 2011 @ 16 h 16 min
  10. I wasn’t thinking much about if-let. Since an if-let has two branches, it seemed to me that you don’t save a whole lot with a special if-let syntax over:

    (cond
    :let [x (seq coll)]
    x true-case

    But you make a good point about the use-case of (if-let [[x & xs] (seq coll)] …) which I wasn’t thinking of because I don’t use it much, but now that you mention it, it’s definitely a common idiom that should ideally be supported.

    Your idea of destructuring in the test as a way of handling if-let is a clever idea. I like it, although I’m not 100% sure it coexists elegantly with the other :let syntax. I wonder if there’s a way to unify the two ideas.

    No matter what, *any* of these proposals I like better than the existing way to do things in Clojure. I think this is an important problem to solve, and any of these ideas are a big step in the right direction.

    Comment by Mark — 19 June 2011 @ 20 h 15 min
  11. Upon further reflection, I think I see a potential weakness with the idea of supporting if-let via destructuring in the test case.

    In general, the when an if-let test succeeds, *that’s* the case that continues to have additional complexity and will do something complicated. Typically the nil case has a simpler result.

    So in reality, you end up with something like:
    (cond
    [[x & xs] (seq coll)] complicated-expression

    and if complicated-expression needs further let/if/when/cond/when-let, you’re back where you started, with extra indentation needed.

    So I don’t know if it’s possible to solve the if-let issue.

    Comment by Mark — 19 June 2011 @ 20 h 31 min
  12. So in case it wasn’t clear from my earlier proposal about :when-let, I was imagining that the line
    :when-let [bindings]

    would be equivalent to the two lines

    :let [bindings]
    (nil? bindings) nil

    In other words, it’s an instruction to bind and then bail out if nil, otherwise continue with the cond processing. Maybe you’re right that :when-let is suggestive of some kind of different behavior than this. I don’t know what would be a better name though. Maybe :guarded-let ? Nah, too verbose but that seems to be the right idea.

    Just for fun, it’s interesting to look at how your cond macro itself would look with the addition of the :let/:when-let ideas:

    (when-let [[test expr & clauses] (seq clauses)]
    (if (= :let test)
    `(let ~expr (cond ~@clauses))
    `(if ~test ~expr (cond ~@clauses)))))

    becomes

    (cond
    :when-let [[test expr & clauses] (seq clauses)]
    (= :let test) `(let ~expr (cond ~@clauses))
    :else `(if ~test ~expr (cond ~@clauses)))

    Not much of a savings for just one when-let/if mixture, but to my eye, even on this small case it is at least as clear, and arguably more so because it has less indentation. For larger blocks of code, I think the improvement would be even more dramatic.

    Comment by Mark — 19 June 2011 @ 20 h 43 min
  13. Mark, your :when-let proposal was perfectly clear, I was just pondering on the name of the keyword that at first I didn’t find fluent in the context of cond.

    I incorporated all our propositions and here comes the Abominable Cond: https://github.com/cgrand/parsley/blob/lr-plus/src/net/cgrand/parsley/util.clj

    It supports:
    * an odd number of arguments (no need for :else — like case and condp)
    * :let and :when-let
    * destructuring vectors as test expressions
    * destructuring vectors (in test position or with :when-let) supports :let and acts as a conjuctions between all expressions (except those in :let obviously) — if-let and when-let works like that too.

    I propose you try to use it with your own code so that we can figure what’s useful, what’s not. My first conclusion is that :when-let isn’t bad once you get used to it and that destructuring vectors are handy but rarely to collapse nested if-lets (because it requires the else forms to be identical).

    Comment by cgrand — 20 June 2011 @ 20 h 32 min
  14. Just wanted to let you know that I’m using the uber cond, and enjoying it. I find that one other useful extra option for the cond-DSL is just a straight :when.

    So
    (cond
    :when (> 2 1)
    …)

    transforms to
    (when (> 2 1) (cond …))

    or alternatively
    (cond
    (not (> 2 1)) nil
    …)

    Comment by Mark — 11 August 2011 @ 4 h 04 min
  15. Here’s the code:
    (defmacro cond
    “A variation on cond which sports let bindings and implicit else:
    (cond
    (odd? a) 1
    :let [a (quot a 2)]
    (odd? a) 2
    3).
    Also supports :when, :when-let and binding vectors as test expressions.”
    [& clauses]
    (when-let [[test expr & more-clauses] (seq clauses)]
    (if (next clauses)
    (if (= :let test)
    `(let ~expr (cond ~@more-clauses))
    (if (= :when test)
    `(when ~expr (cond ~@more-clauses))
    (if (= :when-let test)
    `(when-let ~expr (cond ~@more-clauses))
    (if (vector? test)
    `(if-let ~test ~expr (cond ~@more-clauses))
    `(if ~test ~expr (cond ~@more-clauses))))))
    test)))

    Comment by Mark — 11 August 2011 @ 4 h 20 min
  16. Mark, great minds think alike, see: I added :when a while ago too https://github.com/cgrand/parsley/blob/master/src/net/cgrand/parsley/util.clj#L30 (I forgot to update the docstring though).

    Do you use every feature of the über cond? Is yours based on an enhanced if-let too?

    Comment by cgrand — 11 August 2011 @ 9 h 23 min
  17. I’m using the same code you posted here previously.
    I find the :let and :when enhancements to be frequently used.
    I don’t think I’ve had a reason to use :when-let within the cond (although I can imagine doing so), and I definitely haven’t used the binding vector which translates to an if-let.
    (That’s probably due to my own personal tendency not to mix destructuring bindings with tests. I tend to only use if-lets and when-lets in scenarios where I’m binding a single name to the output of a function that returns nil if it fails.)

    I didn’t think I’d make use of the fact that your version of the cond doesn’t require an :else for the last case, but I do find myself using it sometimes since it’s there.

    Comment by Mark — 12 August 2011 @ 2 h 14 min
  18. I’m really liking this, but isn’t this heading to a “return” concept such as java has. Not that I think that would be a bad thing, it’s excellent for reducing indentation. (Which I do believe is a bit of an issue in idiomatic Clojure code.)

    Comment by Julian Birch — 23 October 2011 @ 11 h 22 min
  19. chủ tịch hải dương

    Clojure and me » A flatter cond

    Trackback by chủ tịch hải dương — 7 October 2023 @ 15 h 42 min

RSS feed for comments on this post. TrackBack URI

Leave a comment

(c) 2024 Clojure and me | powered by WordPress with Barecity