Search code examples
clojureleiningen

Clojure's load-string works in the repl but not in `lein run`


When I start a repl with lein repl I can run the function greet and it works as expected.

(ns var-test.core
  (:gen-class))

(declare ^:dynamic x)

(defn greet []
  (binding [x "Hello World."]
    (println (load-string "x"))))

(defn -main [& args]
  (greet))

But if run the code via lein run it fails with

java.lang.RuntimeException: Unable to resolve symbol: x in this context.

What am I missing?

Is the var x dropped during compilation, despite being declared, since it is never used outside of the string?

Edit:

Solution

@amalloy's comment helped me understand I need to bind *ns* in order load the string within the expected namespace, instead of a new, empty namespace.

This works as expected:

(ns var-test.core
  (:gen-class))

(declare ^:dynamic x)

(defn greet []
  (binding [x "Hello World."
            *ns* (find-ns 'var-test.core)]
    (println (load-string "x"))))

(defn -main [& args]
  (greet))

Solution

  • Wow, I've never seen that function before!

    According to the docs, load-string is meant to read & load forms one-at-a-time from an input string. Observe this code, made from my favorite template project:

    (ns tst.demo.core
      (:use tupelo.core tupelo.test)
      (:require [tupelo.string :as str]))
    
    (dotest
      (def y "wilma")
      (throws? (eval (quote y)))
      (throws? (load-string "y"))
    

    So it appears that load-string starts with a new, empty environment, then reads and evaluates forms one at a time in that new env. Since your x is not in that new environment, it can't be found and you get an error.

    Try it another way:

      (load-string
        (str/quotes->double
    
          "(def ^:dynamic x)
           (binding [x 'fred']
             (println :bb (load-string 'x'))) " ))
    
      ;=>  :bb fred
    

    In this case, we give all the code as text to load-string. It reads and eval's first the def, then the binding & nested load-string forms. Everything works as expected since the working environment contains the Var for x.

    Some more code illustrates this:

    (spy :cc
      (load-string
        "(def x 5)
         x "))
    

    with result

    :cc => 5
    

    So the eval produces the var x with value 5, then the reference to x causes the value 5 to be produced.


    To my surprise, the partial load-string works in a fresh REPL:

    demo.core=> (def x "fred")
    #'demo.core/x
    demo.core=> (load-string "x")
    "fred"
    

    So load-string must be coded to use any pre-existing REPL environment as the base environment. When using lein run, there is no REPL environment available, so load-string starts with an empty environment.