Share your thoughts in the 2024 State of Clojure Survey!

Welcome! Please see the About page for a little more info on how this works.

+1 vote
in Compiler by

Consider this basic project:

(ns whatever.core
  (:gen-class))

(def some-var (doto (System/getenv "FOO") println))

(defn -main [& args] (println some-var))

If I make an uberjar out of this, I can see that during compilation, nil is printed (as expected). However, when I run it with environment variables, I expect nil, but I can see that the correct value is printed and some-var is actually re-bound! Even though that sounds kind of 'convenient' (for this use-case), I fail to understand why it happens...Since I AOT'ed the namespace, I would expect to have to defer getting the env-var until runtime (e.g. via delay or something), for this work. Also what are the implications of this double loading? What if I had some very expensive computation in a def, and really wanted to pre-compute it once (and only once)? From what I can tell all def expressions (reachable from -main) are loaded anew when the program starts, regardless of AOT... Help me understand this please, as it's driving me crazy!

Many thanks in advance...

1 Answer

+1 vote
by

There's no double loading or reloading. Code compilation in Clojure actually runs that code (meaning, it runs all the top-level forms, it won't actually run your -main function unless it's called at the top level). And running the code in an uberjar, well, also runs the code. It's a completely separate step, separate loading.

what are the implications of this double loading?

The main one is that a namespace declaration should be free of side-effects and referentially transparent (barring implicitly changing the current namespace by all the defs).

What if I had some very expensive computation in a def, and really wanted to pre-compute it once (and only once)?

If you don't want for the expensive computation to be run at compile time, you can use delay.
If you want to inline the results of a computation during compilation, you can use a macro.

by
> And running the code in an uberjar, well, also runs the code

But execution should start from the `-main` method, no? Why are the Vars rebound? Is it because the root binding is null, so they are assumed unbound ,which causes the expression to be re-evaluated?

> If you don't want for the expensive computation to be run at compile time, you can use delay.

Yes sure, but that is beyond the point - shouldn't I be able to do that with a plain `def`? Say I'm calculating some nth fibonacci number which takes several seconds. Why would I pay this cost both at compile AND run time? The latter doesn't make sense, when I have AOT compiled, no? what am I missing?
by
> But execution should start from the `-main` method, no?

The execution of the _main_ functionality, yes. Running all the `ns` and `def` forms is also execution. Running any other kind of top-level code is also execution. Clojure compilation and execution are intertwined - it's different from how many other languages handle things. That's one of the reasons why we have great REPL experience where the "E" part is indistinguishable from having that same code in some uberjar from the get go. And vice versa - running compiled artifacts under normal circumstances will not be different from running the original code via REPL, form by form in the right order.

> Why are the Vars rebound?

They are not _re_-bound. They're just bound. An uberjar compilation process and a process that uses that uberjar are two different processes, with two different run-time versions of the same var. And it has nothing to do with the value of a var.

Maybe the second paragraph in this section will be helpful: https://clojure.org/reference/evaluation

> Say I'm calculating some nth fibonacci number which takes several seconds. Why would I pay this cost both at compile AND run time?

First of all, calculating a Fibonacci number of any N shouldn't take several seconds. :) But that's beside the point, of course.
The "why" part has no straightforward answer because that was not a deliberate decision like "yes, the cost must be paid twice, users must suffer". It is simply a consequence of Clojure's compilation/evaluation approach.

The vast majority of time things like this are completely unimportant because all of the top-level code is plain referentially transparent `def`s that don't take any noticeable time to be computed.
If for some reason that's not the case in some very specific case, there are solutions for that that I've mentioned - `delay` and macros.
by
So, you're saying that deferring to get an environment variable until runtime (like below), is completely unnecessary?

>  (def some-var (delay (System/getenv "FOO")))
by
It's unnecessary if the environment where you compile the project won't affect the compilation itself. An example of the contrary - you can have `some-var` set at compile time to the value of some env var and then use `some-var` in a macro to determine how to expand something. Highly not recommended, of course.
by
Many thanks for your time - much appreciated :)
...