Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why I can set! builtin dynamic (?) Clojure vars?

Why I can do this:

> (set! *unchecked-math* true)
true
> (set! *warn-on-reflection* false)
false

but can't do this:

> (def ^:dynamic *x*)
#'user/*x*
> (set! *x* 1) ;; no luck, exception!
like image 809
OlegTheCat Avatar asked Aug 10 '16 15:08

OlegTheCat


People also ask

How do you define a variable in Clojure?

In Clojure, variables are defined by the 'def' keyword. It's a bit different wherein the concept of variables has more to do with binding. In Clojure, a value is bound to a variable.

What is binding in Clojure?

binding => var-symbol init-expr Creates new bindings for the (already-existing) vars, with the supplied initial values, executes the exprs in an implicit do, then re-establishes the bindings that existed before.

What is Clojure namespace?

A namespace is both a name context and a container for vars. Namespace names are symbols where periods are used to separate namespace parts, such as clojure. string . By convention, namespace names are typically lower-case and use - to separate words, although this is not required.


3 Answers

Is it possible the builtin dynamics are implicitly wrapped in a binding form by the runtime? Because this works, for example:

user=> (def ^:dynamic *x*)
user=> (binding [*x* false] (set! *x* true))
true
user=> 

One thing to note is that the documentation explicitly says that its an error to try and modify the root binding via set!, see:

http://clojure.org/reference/vars

It's also possible that the builtins are treated exceptionally, for example if you look at the metadata of x:

user=> (meta #'*x*)
{:dynamic true, :line 1, :column 1, :file "/private/var/folders/8j/ckhdsww161xdwy3cfddjd01d25k_1q/T/form-init5379741350621280680.clj", :name *x*, :ns #object[clojure.lang.Namespace 0x6b8f00 "user"]}

It's marked as dynamic, whereas *warn-on-reflection* is not marked as dynamic but still works in a binding form:

user=> (meta #'*warn-on-reflection*)
{:added "1.0", :ns #object[clojure.lang.Namespace 0x377fc927 "clojure.core"], :name *warn-on-reflection*, :doc "When set to true, the compiler will emit warnings when reflection is\n  needed to resolve Java method calls or field accesses.\n\n  Defaults to false."}
user=> (binding [*warn-on-reflection* true] (set! *warn-on-reflection* false))
false
user=>

Presumably this is for backward compatibility because in earlier versions of clojure vars with earmuffs (stars in each side) were dynamic by convention. But anyway this just goes to show that builtins are treated slightly differently.

Now, I decided to take this a bit further, and grep the source code of clojure, looking for warn-on-reflection, which leads me to the constant WARN_ON_REFLECTION, which leads me to lines of code like this in RT.java:

https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/RT.java#L467

Var.pushThreadBindings(
        RT.mapUniqueKeys(CURRENT_NS, CURRENT_NS.deref(),
               WARN_ON_REFLECTION, WARN_ON_REFLECTION.deref()
                ,RT.UNCHECKED_MATH, RT.UNCHECKED_MATH.deref()));

Which leads me to believe that my initial assumption was correct, that certain special global vars are implicitly wrapped in a thread local binding.

EDIT:

As mentioned in a comment, you can use clojure.core/push-thread-bindings, but be sure to follow the advice of the documentation and wrap in try/catch/finally with pop-thread-bindings in the finally block. And at that point you would have just re-implemented binding (e.g. run (source binding) at the repl), which is probably why the doc explicitly warns that push-thread-bindings is a low level function and binding should be preferred.

like image 158
Kevin Avatar answered Oct 06 '22 21:10

Kevin


Perusing the doc, you'll find

user=> (doc thread-bound?)
-------------------------
clojure.core/thread-bound?
([& vars])
  Returns true if all of the vars provided 
  as arguments have thread-local bindings.
  Implies that set!'ing the provided vars will succeed.  
  Returns true if no vars are provided.

in particular:

Implies that set!'ing the provided vars will succeed

so, this means you can check if set! is possible as follows:

user=> (thread-bound? #'*x*)
false
user=> (thread-bound? #'*unchecked-math*)
true     

which means you can only set! thread-bound vars, which your *x* is not (yet).


PS: in Kevins answer you'll see Var.pushThreadBindings which presumely is actually avaliable as clojure.core/push-thread-bindings - if you wan't to dig deeper.

like image 36
birdspider Avatar answered Oct 06 '22 20:10

birdspider


According to the reference on Vars, you can only use set! assignment on thread-bound vars:

Currently, it is an error to attempt to set the root binding of a var using set!, i.e. var assignments are thread-local. In all cases the value of expr is returned.

However, the built-in dynamic vars like *warn-on-reflection* have thread-local bindings, thus you can freely use set! on them:

(thread-bound? #'*unchecked-math*)
=> true

whereas (def ^:dynamic *x*) only creates a root binding:

(thread-bound? #'*x*)
=> false

The binding macro creates a new scope for a dynamic Var; on the low level, it temprorarily 'pushes' bound values of given Vars to the current thread, and then 'pops' them upon exiting the body of the macro.

(binding [*x* 1]
  (thread-bound? #'*x*))
=> true

(binding [*x* 1]
  (set! *x* 2))
=> 2
like image 1
superkonduktr Avatar answered Oct 06 '22 21:10

superkonduktr