Sharas
Sharas

Reputation: 905

Clojure's eval does not "see" local symbols

I am experimenting with eval in Clojure:

(let [code_as_data '(if (< sequ) on_true on_false)
      sequ [1 3 5]
      on_true "sequence is sorted in ascending order"
      on_false "sequence is NOT sorted"]
  (eval code_as_data))

CompilerException java.lang.RuntimeException: Unable to resolve symbol: sequ in this context, compiling:(/tmp/form-init3253735970468294203.clj:1:25)

How do I define symbols so that they are "seen" by eval?

Upvotes: 2

Views: 465

Answers (3)

mange
mange

Reputation: 3212

Through the unholy magic of macros, you can actually construct a version of eval that mostly does what you want it to.

(defmacro super-unsafe-eval
  "Like `eval`, but also exposes lexically-bound variables to eval. This
  is almost certainly a bad idea."
  [form]
  `(eval (list 'let
               ~(vec (mapcat #(vector `(quote ~%)
                                      `(list 'quote ~%))
                             (keys &env)))
               ~form)))

This macro uses the special &env variable to access the local environment. It then constructs a let form which binds all the names that are currently bound in the environment that the macro is expanded in. This makes your code sample work:

(let [code_as_data '(if (< sequ) on_true on_false)
      sequ         [1 3 5]
      on_true      "sequence is sorted in ascending order"
      on_false     "sequence is NOT sorted"]
  (super-unsafe-eval code_as_data))
;;=> "sequence is sorted in ascending order"

There's also a slight bug in your program. Calling < with a single argument will always return true. You need to use apply to make it work properly:

(let [code_as_data '(if (apply < sequ) on_true on_false)
      on_true      "sequence is sorted in ascending order"
      on_false     "sequence is NOT sorted"]
  [(let [sequ [1 3 5]]
     (super-unsafe-eval code_as_data))
   (let [sequ [1 3 1]]
     (super-unsafe-eval code_as_data))])
;;=> ["sequence is sorted in ascending order" "sequence is NOT sorted"]

Upvotes: 0

noisesmith
noisesmith

Reputation: 20194

The simplest way to provide local data to code generated at runtime by eval is to generate a form that takes arguments.

(let [code-as-data '(fn [sequ on-true on-false]                                 
                      (if (apply < sequ)                                        
                        on-true                                                 
                        on-false))                                              
      f (eval code-as-data)]                                                    
  (f [1 3 5]                                                                    
     "sequence is sorted in ascending order"                                    
     "sequence is NOT sorted"))

Of course, since functions are our standard means of inserting runtime values into known forms, this really doesn't need to use eval at all. The same functionality can be expressed more simply without eval:

(let [f (fn [sequ on-true on-false]                                             
          (if (apply < sequ)                                                    
            on-true                                                             
            on-false))]                                                         
  (f [1 3 5]                                                                    
     "sequence is sorted in ascending order"                                    
     "sequence is NOT sorted"))

In actual code, the eval version is only needed if the logic needs to be generated at runtime (for example if a user provides a new algorithm). If it is onerous to expect users to write their code as a function, you can do a compromise:

(defn code-with-context                                                         
  [body sq t else]                                                              
  (let [f (eval (list 'fn '[sequ on-true on-false] body))]                      
    (f sq t else)))                                                             



(code-with-context (read-string "(if (apply < sequ) on-true on-false)")         
                   [1 3 5]                                                      
                   "sequence is sorted in ascending order"                      
                   "sequence is NOT sorted")

Upvotes: 3

leetwinski
leetwinski

Reputation: 17849

Eval does not recognize lexical bindings (local ones, like with let), though it recognizes the global/dynamic ones. So one of the solutions is to predefine dynamic vars and eval in dynamic binding context:

user> (def ^:dynamic sequ)
#'user/sequ

user> (def ^:dynamic on_true)
#'user/on_true

user> (def ^:dynamic on_false)
#'user/on_false

user> 
(let [code_as_data '(if (apply < sequ) on_true on_false)]
  (binding [sequ [1 3 5]
            on_true "sequence is sorted in ascending order"
            on_false "sequence is NOT sorted"]
    (eval code_as_data)))
"sequence is sorted in ascending order"

(notice one little mistake: you use (< sequ) which always returns true, what you need is to (apply < sequ))

as you can see, it is quite ugly, and you don't really want to use it. One of the possible workarounds is to substitute data into the evaluated code using syntax quoting:

user> 
(let [sequ [1 3 5]
      on_true "sequence is sorted in ascending order"
      on_false "sequence is NOT sorted"
      code_as_data `(if (apply < ~sequ) ~on_true ~on_false)]
  (eval code_as_data))

"sequence is sorted in ascending order"

another option (that looks more usable to me) is to replace all the symbols you need with their values using walker:

user> 
(let [code_as_data '(if (apply < sequ) on_true on_false)
      bnd {'sequ [1 3 5]
           'on_true "sequence is sorted in ascending order"
           'on_false "sequence is NOT sorted"}]
  (eval (clojure.walk/postwalk-replace bnd code_as_data)))

"sequence is sorted in ascending order"

Upvotes: 2

Related Questions