Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does Clojure ^:const work?

I'm trying to understand what ^:const does in clojure. This is what the dev docs say. http://dev.clojure.org/display/doc/1.3

(def constants {:pi 3.14 :e 2.71})

(def ^:const pi (:pi constants)) (def ^:const e (:e constants))

The overhead of looking up :e and :pi in the map happens at compile time, as (:pi constants) and (:e constants) are evaluated when their parent def forms are evaluated.

This is confusing since the metadata is for the var bound to symbol pi, and the var bound to symbol e, yet the sentence below says it helps speed up the map lookups, not the var lookups.

Can someone explain the what ^:const is doing and the rationale behind using it? How does this compare to using a giant let block or using a macro like (pi) and (e)?

like image 782
bmillare Avatar asked Feb 06 '12 15:02

bmillare


3 Answers

That looks like a bad example to me, since the stuff about map-lookup just confuses the issue.

A more realistic example would be:

(def pi 3.14)
(defn circ [r] (* 2 pi r))

In this case, the body of circumference is compiled into code that dereferences pi at runtime (by calling Var.getRawRoot), each time circumference is called.

(def ^:const pi 3.14)
(defn circ2 [r] (* 2 pi r))

In this case, circ2 is compiled into exactly the same code as if it had been written like this:

(defn circ2 [r] (* 2 3.14 r))

That is, the call to Var.getRawRoot is skipped, which saves a little bit of time. Here is a quick measurement, where circ is the first version above, and circ2 is the second:

user> (time (dotimes [_ 1e5] (circ 1)))
"Elapsed time: 16.864154 msecs"
user> (time (dotimes [_ 1e5] (circ2 1)))
"Elapsed time: 6.854782 msecs"
like image 56
Chris Perkins Avatar answered Nov 04 '22 02:11

Chris Perkins


Besides the efficiency aspect described above, there is a safety aspect that is also useful. Consider the following code:

(def two 2)
(defn times2 [x] (* two x))
(assert (= 4 (times2 2)))    ; Expected result

(def two 3)                  ; Ooops! The value of the "constant" changed
(assert (= 6 (times2 2)))    ; Used the new (incorrect) value

(def ^:const const-two 2)
(defn times2 [x] (* const-two x))
(assert (= 4 (times2 2)))    ; Still works

(def const-two 3)            ; No effect!
(assert (= 3 const-two ))    ; It did change...
(assert (= 4 (times2 2)))    ; ...but the function did not.

So, by using the ^:const metadata when defining vars, the vars are effectively "inlined" into every place they are used. Any subsequent changes to the var, therefore, do not affect any code where the "old" value has already been inlined.

The use of ^:const also serves a documentation function. When one reads (def ^:const pi 3.14159) is tells the reader that the var pi is not ever intended to change, that it is simply a convenient (& hopefully descriptive) name for the value 3.14159.


Having said all the above, note that I never use ^:const in my code, since it is deceptive and provides "false assurance" that a var will never change. The problem is that ^:const implies one cannot redefine a var, but as we saw with const-two it does not prevent the var from being changed. Instead, ^:const hides the fact that the var has a new value, since const-two has been copied/inlined (at compile-time) to each place of use before the var is changed (at run-time).

A much better solution would be to throw an Exception upon attempting to change a ^:const var.

like image 32
Alan Thompson Avatar answered Nov 04 '22 00:11

Alan Thompson


In the example docs they are trying to show that in most cases if you def a var to be the result of looking something up in a map without using const, then the lookup will happen when the class loads. so you pay the cost once each time you run the program (not at every lookup, just when the class loads). And pay the cost of looking up the value in the var each time it is read.

If you instead make it const then the compiler will preform the lookup at compile time and then emit a simple java final variable and you will pay the lookup cost only once total at the time you compile the program.

This is a contrived example because one map lookup at class load time and some var lookups at runtime are basically nothing, though it illustrates the point that some work happens at compile time, some at load time, and the rest well ... the rest of the time

like image 11
Arthur Ulfeldt Avatar answered Nov 04 '22 00:11

Arthur Ulfeldt