4

In Clojure, is there a way to make a var constant such that it can be used in case statements?

e.g.

(def a 1)
(def b 2)

(let [x 1]
  (case x
    a :1
    b :2
    :none))
=> :none

I understand I can use something like cond or condp to get around this, but it would be nice if I could define something that does not require further evaluation so I could use case.

leeor
  • 17,041
  • 6
  • 34
  • 60
  • using a hash-map requires no further evaluation, and compiles to something pretty close to what a case would compile to – noisesmith Aug 30 '15 at 20:05
  • @noisesmith A hash-map needs to be instantiated and looked up. Both requires evaluation. Implementation wise hash-map lookup is not even close to what case compiles to. – Leon Grapenthin Aug 30 '15 at 20:26
  • The instantiation can happen at compile time, if we are emulating a case statement. My understanding was that a case statement compiled to a hash lookup in bytecode but don't have the proof handy. – noisesmith Aug 30 '15 at 20:28
  • @LeonGrapenthin [this function](https://github.com/clojure/clojure/blob/clojure-1.7.0/src/clj/clojure/core.clj#L6327) leads me to believe case does in fact compile to a hash lookup. – noisesmith Aug 30 '15 at 20:32
  • @noisesmith Admittedly there is more hashing going on than I originally thought. Still not convinced on the "pretty close" part, especially in this case where the constants are all numbers. – Leon Grapenthin Aug 30 '15 at 22:51

2 Answers2

4

Related and answer stolen from it:

As the docstring tells you: No you cannot do this. You can use Chas Emericks macro and do this however:

(defmacro case+
  "Same as case, but evaluates dispatch values, needed for referring to
   class and def'ed constants as well as java.util.Enum instances."
  [value & clauses]
  (let [clauses (partition 2 2 nil clauses)
        default (when (-> clauses last count (== 1))
                  (last clauses))
        clauses (if default (drop-last clauses) clauses)
        eval-dispatch (fn [d]
                        (if (list? d)
                          (map eval d)
                          (eval d)))]
    `(case ~value
       ~@(concat (->> clauses
                   (map #(-> % first eval-dispatch (list (second %))))
                   (mapcat identity))
           default))))

Thus:

(def ^:const a 1)
(def ^:const b 2)
(let [x 1]
  (case+ x
    a :1
    b :2
    :none))
=> :1

An alternative (which is nice since it's more powerful) is to use core.match's functionality. Though you can only match against local bindings:

(let [x 2
      a a
      b b]
  (match x
    a :1
    b :2
    :none))
 => :2
Community
  • 1
  • 1
ClojureMostly
  • 4,652
  • 2
  • 22
  • 24
  • I thought ^:const would replace the vars with their respective literals at compilation time. Is there a way to enforce that? – D.Ginzbourg Apr 23 '20 at 09:21
2

You can also use clojure.core/condp for the job:

(def a 1)
(def b 2)
(let [x 1]
  (condp = x
    a :1
    b :2
    :none))
#=> :1
mike3996
  • 17,047
  • 9
  • 64
  • 80