Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clojure global variable behavior in a threaded environment

Given that this works as I'd expect:

(do
  (println (resolve 'a)) ; nil 
  (def a "a")
  (println (resolve 'a))) ; #'user/a

I'd like to understand why this doesn't:

(future
  (println (resolve 'b)) ; #'user/b (shouldn't it be still undefined at this point?)
  (def b "b")
  (println (resolve 'b))) ; #'user/b

I'd also like to know if this is a proper solution (not exactly solving the same problem, but doing an equivalent job in my context):

(def c (atom nil))
(future
  (println @c) ; nil
  (reset! c "c")
  (println @c)) ; c
like image 302
cjauvin Avatar asked Aug 21 '12 17:08

cjauvin


1 Answers

This behaviour comes about as a result of the way in which def forms are compiled.

Note that using def forms not at top-level (or perhaps inside a top-level let -- see below for more comments on this case) is frowned upon as a matter of style in any case. The snippet using an Atom, on the other hand, is fine -- no reason not to use it if it does what you want.

On to the def story:

  1. Compilation of def forms:

    When a def form is encountered, a Var of the appropriate name is created at that moment by the compiler in the current namespace. (Attempting to def a Var outside the current namespace by using a namespace-qualified symbol as the name argument to def results in an exception). That Var is at first unbound and stays unbound until the def is actually executed; for a top-level def, that'll be right away, but for a def hidden inside a function's body (or inside a let form -- see below), that'll be when the function is called:

    ;;; in the user namespace:
    
    (defn foo []
      (def bar "asdf")
     :done)
    ; => #'user/foo
    
    bar
    ; => #<Unbound Unbound: #'user/bar>
    
    ;;; let's change the namespace and call foo:
    
    (ns some.ns)
    (user/foo)
    ; => :done
    
    bar
    ; exception, the bar Var was created in the user namespace!
    
    user/bar
    ; => "asdf"
    ; the Var's namespace is fixed at compile time
    
  2. The first example -- with the do form:

    Top level dos are treated as if their contents were spliced into the flow of code at the place where the do occurs. So if you type (do (println ...) (def ...) (println ...)) at the REPL, that's equivalent to typing in the first println expression, then the def, then the second println expression (except the REPL only produces one new prompt).

  3. The second example -- with future:

    (future ...) expands to something close to (future-call (fn [] ...)). If ... includes a def form, it'll be compiled in the manner we have seen above. By the time the anonymous function executes on its own thread the Var will have been created, thus resolve will be able to find it.

  4. As a side note, let's have a look at a similar snippet and its output:

    (let []
      (println (resolve 'c)) 
      (def c "c") 
      (println (resolve 'c)))
    ; #'user/c
    ; #'user/c
    ; => nil
    

    The reason is as before with the extra point that let is first compiled, then executed as a whole. This is something one should keep in mind when using top-level let forms with definitions inside -- it's generally ok as long as no side-effecty code is intermingled with the definitions; otherwise one has to be extra careful.

like image 113
Michał Marczyk Avatar answered Oct 01 '22 23:10

Michał Marczyk