Is use of (case) a shibboleth?

Whenever I see a codebase using case I think, “This must be someone who hasn’t yet gained fluency in Clojure. I would expect any Lisper to use condp instead.” Nonetheless, @bbatsov 's esteemed style guide suggests use of case when dealing with compile-time constants, which doesn’t particularly make a case to me (no pun intended). What do you think? What are reasons to use of (case) besides, “Well, I use it in my other languages…”?

The benefit of case is that unlike cond and condp, it is not a sequential search for a match – the match is O(1). From an implementation point of view, all of the match expressions become keys in a map and the match occurs via a lookup on that map. source

3 Likes
(let [x :a]
  (time (dotimes [i 1000000]
          (case x
            :something :blah
            :not-a :blee
            :a :success
            :fail))))
;;"Elapsed time: 10.350213 msecs"

;;best case
(let [x :a]
  (time (dotimes [i 1000000]
          (condp = x
            :a :success
            :something :blah
            :not-a :blee
            :fail))))
;;"Elapsed time: 10.272783 msecs"

;;slightly worse
(let [x :a]
  (time (dotimes [i 1000000]
          (condp = x           
            :something :blah
            :a :success
            :not-a :blee
            :fail))))
;;"Elapsed time: 60.965878 msecs"

;;worst
(let [x :a]
  (time (dotimes [i 1000000]
          (condp = x            
            :something :blah
            :not-a :blee
            :a :success
            :fail))))
;;"Elapsed time: 110.159715 msecs"

For broad selection possibilities, case is king. Fast and elegant. I even use case in some places where there’s a only a single possibility but I don’t want to bind let or if-let stuff, out of aesthetic preference

;;less to type IMO
(let [m {:a 2 :b 3}]
  (case (:a m)
    2 :two
    :not-two!))

;;more to type, semantically and performance-wise
;;equivalent (altough identical? would be faster)
(let [m {:a 2 :b 3}]
  (condp = (:a m)
    2 :two
    :not-two!))

;;typical if based way.
(let [m {:a 2 :b 3}]
  (if (= (:a m) 2)
    :two
    :not-two!))
3 Likes

Why would you use condp where a case would suffice? Use the least powerful construct that does the job.

2 Likes

Why would you? I’m curious to hear the flip side.

Otherwise, I don’t personally see anything wrong with case. It is faster than condp, and in most cases I find it’s easier to read. With condp, you have to decode the predicate to understand what kind of branching will be performed, which adds a bit of time to my mental parsing of such a statement when reading code.

Well I do believe I have achieved reasonable fluency in Clojure by now - I’m not sure that’s enough to make graduate to being a ‘true Lisper’, but I don’t think that’s a very important characterisation either. I can at least say that I am well aware of both case and condp, and have more frequently chosen the former in the codebases I’ve contributed to.

Viewing a function / operation as an interpreter of its argument, condp interprets a more expressive language than case (arbitrary predicates over constants). As such, a call to condp can a priori express a wider range of behaviours than case, which means the human reader has more possibilities to rule out and more sophisticated inference to do when trying to understand the code. In more down-to-earth words: case is generally dumber, therefore more predictable and easier to understand. This is why I favour case in those situations where it’s expressive enough to be an alternative to condp.

I would generally recommend against over-abstraction or looking for the ‘clever’ solutions - not in the spirit of being a Lisper, just of programming pragmatically. Powerful constructs are useful, but should be used sparingly.

2 Likes

Excellent points everywhere. I didn’t realize there were performance differences underlying case vs condp. My original impression of case was that it was a poor-man’s condp. Your answers have shown that I was right – but as a result of its poverty, case is both cognitively and computationally cheaper, which can be a good thing.

As for why I thought it was non-lispy, it was because I saw it as a hold-over that does the same thing as condp, but was made to favor visitors from other languages who used case-switch in C or Java or wherever. I think this thread has made some good refutations of that idea, though. The resultant question becomes “When should condp be used?” and I suppose the answer is, “whenever the branch is more complex than a simple equality check”

2 Likes

In Common Lisp, they call case a dispatching construct, because you can think of it more like an inline version of defmulti. That is, the evaluated form returns the name of the branch to dispatch too.

case chooses one of its clauses to execute by comparing a value to various constants. Where as condp runs the branch whose predicate first evaluates to true. Thinking of it that way, you can see that condp doesn’t dispatch, though it can be made to behave that way if you use equal as your predicate. In that case, it will still evaluate the predicate for each clause. That last detail is the clue to when you want condp over case. Basically, for case you need to have the clauses be a constant, since they won’t be evaluated. If you need them evaluated is when you’d reach for condp instead. Also, if you don’t need to dispatch on equality, but instead want to just branch to the first true clause of a more complex predicate, like say greater than or some other logic, condp can be used.

2 Likes