We can reload any function and/or variable in Clojure at runtime almost instantly. We can even change method signatures. The most we can do with Scala or Java is to use JRebel which is slow, commercial, and restricted. What is the difference that allows Clojure to be so interactive? Reading about this in Slack, I have found the following comments, but I wish to know more about it. Links to papers/articles clarifying the issue further is also appreciated (though not required).
It’s mostly because the language is set up to be reloadable. Clojure has a var indirection for every function or top level variable definition which you can mutate, so you can redefine just one function while keeping the rest of your environment the same and carry on
.
following up on that - there's indirection when the function name is in the code, but for a long running function that took another function as an argument (eg. you passed a handler function to an http server process startup) you can get the benefits of var indirection by hand - by passing #'handler instead of handler but otherwise you don't get the reloading (without restarting the process that took that arg)
.
kind of
direct linking replaces var calls being compiled with direct calls (edited) the var path however still exists and NEW code can still invoke via the vars
The key to what you're asking lies on how Clojure identifies functions and runs them at runtime. First, Clojure functions are defined as vars
, which is the Clojure name for their JVM root class, Var
.
Clojure's runtime maintains a single ConcurrentHashMap
called Namespaces
. This map has Symbol
keys (the namespace name) and Namespace
values. Each Namespace
in turn has an AtomicReference
'd Clojure map
(called "mappings") that is dynamically typed but which essentially has Clojure Symbol
keys (the local variable name) and Var
values.
When you invoke a Clojure function, it first looks up which namespace you're referencing in Namespaces
and then looks up the specific variable in that namespace's mappings. This makes hot-loading code trivial - all you need to do is set a new <Symbol, Var>
pair on a given namespace's mappings.
To go one level deeper, Clojure also maintains an awareness of "frames" (i.e. threads or additional bindings that might temporarily re-define variables within a local scope). These have their own ThreadLocal
storage and a variable that is found in one of these will be used instead of the variable currently being stored in the namespace's mappings.
Clojure's approach here is possible because it doesn't try to store functions as JVM functions, but rather as Java Objects themselves that are kept in a map that can be rapidly accessed.
Clojure knows these Objects are in fact callable by checking to see if they satisfy a function interface (IFn
). An object satisfies IFn
by having an Invoke
method. This is used for a wide number of pretty clever purposes, and explains why many of Clojure's core data structures (maps, vectors, keywords, etc.) are all also callable as functions.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With