How to make Clojure evaluate constant local expressions at compile time

Running 4Clojure Problem 178 - The best hand I have is for converting map values ​​from characters to numbers:

(fn [ch] (or ({\A 1} ch) ((zipmap "TJQK" (iterate inc 10)) ch) (- (int ch) (int \0)))) 

The zipmap expression is evaluated on every call, always creating {\K 13, \Q 12, \J 11, \T 10} .

How can we make the compiler evaluate it only once?


After long brain-beatings, I came up with

 (defmacro constant [exp] (eval exp)) 

... ending the zipmap call zipmap this:

 (constant (zipmap "TJQK" (iterate inc 10))) 

I think this is equivalent

 (eval '(zipmap "TJQK" (iterate inc 10))) 

... but not up to eval without a quote:

 (eval (zipmap "TJQK" (iterate inc 10))) 

Corrections, comments and improvements are welcome.

+6
source share
1 answer

Your constant macro will work for this particular situation, because the evaluated form has only characters that allow functions in clojure.core and compilation time literals. You may have problems resolving the character in other situations, for example, calculating local constants from function parameters for use in an anonymous function.

A more general way to do this is to move the zipmap call outside of the fn form. One option is to save the results of the zipmap call to var using def , as in (def my-const (zipmap "TJQK" (iterate inc 10))) .

However, in this case, when you create an anonymous function, creating a globally accessible var may be excessive. Therefore, I suggest placing fn inside the let binding, which captures a constant value:

  (let [face-cards (zipmap "TJQK" (iterate inc 10))] (fn [ch] (or ({\A 1} ch) (face-cards ch) (- (int ch) (int \0))))) 

Edit: As pointed out in the comments, this solution will calculate a constant value at boot time, not at compile time. It’s hard for me to come up with a scenario where it matters, but it's worth noting that its semantics are slightly different from your approach.

+5
source

All Articles