in which the perils of the gilardi scenario are overcome



There's an interesting gotcha that's bitten me a few times while exploring the fringes of Clojure. The eval function is not often used, but there are some cases when it's justifiable. It comes up while writing Leiningen plugins since code needs to be constructed in the context of Leiningen and run in the context of a project.

The Gilardi Scenario refers to a case where you want to evaluate some code that both loads in a new var and refers to that var in the same piece of code:

(eval `(when ~(:some-set project)
         (require '~'clojure.set)
          ~(:some-set project) #{:bad-key})))

java.lang.ClassNotFoundException: clojure.set

The problem is that Clojure has to compile this whole block of code as a single unit. At compile time, the clojure.set namespace has not been loaded yet, so the attempt to look up the difference var is doomed to failure.

In Clojure 1.1, the compiler introduced special handling to work around this. If the top-level form being evaled is a do, then it will be broken up and each subform will be evaled separately. Unfortunately this only goes so far; it only applies to top-level dos, so even though our when above macro-expands to a form that contains do, the compiler can't apply its special-case.

Another way around it is to look up the var at runtime. The ns-resolve function works well for that.

(eval `(when ~(:some-set project)
         (require '~'clojure.set)
         ((ns-resolve '~'clojure.set '~'difference)
          ~(:some-set project) #{:bad-key})))

This delays the lookup of clojure.set/difference to runtime, incurring a small penalty but tidily avoiding the Gilardi scenario. It also looks a bit ugly due to the quote-unquote-quote notation. This is necessary inside a backtick since symbols are fully-qualified to avoid accidental aliasing existing names. I'm not sure if this is an intentional design decision of Clojure, but it's certainly a sign that you're off the beaten path and into possibly-inadvisable territory.

Leiningen used to encourage the ns-resolve solution above since it needed to support eval of arbitrary forms from plugins. Even if your form had a do at the top-level to invoke the compiler's special-case, you could easily have situations where add-hook has been used to wrap the form in enclosing forms, making your do nested deeply where the compiler wouldn't split it out.

(defn get-readable-form [java project form init]
  (let [cp (str (.getClasspath (.getCommandLine java)))
        form `(do ~init
                  (def ~'classpath ~cp)
                  (set! ~'*warn-on-reflection*
                        ~(:warn-on-reflection project))
    (prn-str form)))

But the latest version of Leiningen makes it work without ns-resolve. Even though eval-in-project accepts any form to eval, it splices it into an existing do form that places all requires (the init argument in the function above) into the top-level of the do where the compiler can work its magic. So if you've got a namespace that your form is going to depend upon, just pass in '(require 'my.ns) as the init arg to eval-in-project to deftly maneuver your way through the Gilardi Scenario.

« older | 2010-11-10T05:43:43Z | newer »