Ever since I converted from Leiningen and
Boot to
tools.deps, I've been missing a place
to define devtime functions and helpers that would automatically be available in
any REPL I start locally. Boot allows to put any code into profile.boot
,
Leiningen has a system-wide profiles.clj
that is a bit more awkward for
defining functions but it still can be done. I finally decided to recreate the
same experience with tools.deps and got pretty close. The setup I came up with
took a bit of effort to figure out, so I want to document all the steps and
gotchas here and share this setup with you.
Let's begin by creating a file ~/.clojure/user.clj
. For now, its content will
be the following:
(in-ns 'user)
(defn heap []
(let [u (.getHeapMemoryUsage (java.lang.management.ManagementFactory/getMemoryMXBean))
used (/ (.getUsed u) 1e6)
total (/ (.getMax u) 1e6)]
(format "Used: %.0f/%.0f MB (%.0f%%)" used total (/ used total 0.01))))
(println "Loaded system-wide user.clj!")
tools.deps does not have a notion of a special Clojure file that it will load
automatically. But we can instruct it to do so. There are two ways — two
command-line options — for this. --init
(-i
for short) will load the
provided file:
$ clj -i ~/.clojure/user.clj
Loaded system-wide user.clj!
Our user.clj
got loaded, but notice how we didn't drop into the REPL — Clojure
CLI immediately quit after loading the file. We'll have to pass an explicit -r
flag to get the REPL together with the initializing file:
$ clj -i ~/.clojure/user.clj -r
Clojure 1.12.0-alpha5
Loaded system-wide user.clj!
user=> (heap)
"Used: 7/4295 MB (0%)"
Another option is to use the --eval/-e
option and call load-file
with it:
$ clj -e '(load-file (str (System/getProperty "user.home") "/.clojure/user.clj"))' -r
Clojure 1.12.0-alpha5
Loaded system-wide user.clj!
user=> (heap)
"Used: 7/4295 MB (0%)"
So, this is a way to explicitly load system-wide helpers into the REPL. We could
wrap it into shell aliases and call it a day. But there are other things that
you may want to set globally, such as extra dependencies and JVM options. To
satisfy all those requirements, we're going to put the initializing code into
~/.clojure/deps.edn
.
There is a deps.edn parameter
:main-opts that
allows specifying default command-line parameters passed to Clojure CLI.
Unfortunately, top-level :main-opts
is not supported; it has to be within an
alias. Let's make our global deps.edn look like this:
{...
:aliases
{:user
{:main-opts ["-e" "(load-file (str (System/getProperty \"user.home\") \"/.clojure/user.clj\"))"]}}
...}
I prefer -e
here instead of -i
because neither ~
nor $HOME
could be
resolved within deps.edn
, and you would have to hardcode the full path to the
file, making the config less generic and cross-platform[1].
Let's try out the new alias we've defined:
$ clj -M:user -r
Clojure 1.12.0-alpha5
Loaded system-wide user.clj!
user=> (heap)
"Used: 7/4295 MB (0%)"
In order for CIDER to automatically pick up the :user
alias, you need to
execute M-x customize-variable RET cider-clojure-cli-alises
and set it to
:user
. Now, CIDER would append :user
alias whenever you start a REPL with
it.
However, this is still not enough to load user.clj
. CIDER provides its own
:main-opts
when you invoke cider-jack-in
, and since multiple :main-opts
from different aliases don't concatenate but override each other, the
:main-opts
from :user
alias is simply discarded. We have to change one extra
variable, M-x customize-variable RET cider-repl-init-code
, and set its value
to:
'("(when-let [requires (resolve 'clojure.main/repl-requires)] (clojure.core/apply clojure.core/require @requires))"
"(load-file (str (System/getProperty \"user.home\") \"/.clojure/user.clj\"))"
"(in-ns 'user)")
Now, CIDER would load user.clj
as instructed after the REPL starts. I don't
know how this is achieved in other Clojure IDEs, but I'm pretty sure they have a
similar option.
You can still add some shell aliases to simplify launching the REPL from the
terminal. They would look the same in .bash_profile
, .zshrc
, or
fish.config
:
alias clojure="clojure -A:user"
alias clj="clj -A:user"
But since our :user
alias hijacks command line options, you would still have
to launch the REPL as clj -r
. I deal with this minor annoyance by having a
third alias, cljr
, that also enables
rebel-readline, which is much more
powerful than the standard readline. I have an extra alias for it in my
deps.edn
:
{...
:aliases
{...
:rebel {:extra-deps
{com.bhauman/rebel-readline {:mvn/version "0.1.4"}}
:main-opts
["-e" "(load-file (str (System/getProperty \"user.home\") \"/.clojure/user.clj\"))" "-m" "rebel-readline.main"]}}}
See I had to repeat -e (load-file ...)
in :rebel
alias. Again, this is
because :main-opts
don't merge. Then, there is an extra line in my shell
config:
alias cljr="clojure -M:user:rebel"
Finally, just running cljr
in the terminal would launch a REPL with
rebel-readline and my user.clj loaded.
I keep many different helper functions in this global user.clj
. For example,
functions that simplify reflection access to private fields and methods. heap
,
which we've already seen. time+. Because all
those functions are defined under user
namespace, they become globally
accessible as (user/heap)
and so on. Another helper I use all the time loads
performance tools into the current namespace. First, my full :user
alias looks
like this:
{...
:aliases
{:user {:extra-deps
{com.clojure-goes-fast/clj-async-profiler {:mvn/version "1.2.0"}
com.clojure-goes-fast/clj-java-decompiler {:mvn/version "0.3.4"}
com.clojure-goes-fast/clj-memory-meter {:mvn/version "0.3.0"}
criterium/criterium {:mvn/version "0.4.5"}}
:jvm-opts ["-Djdk.attach.allowAttachSelf"
"-XX:+UseG1GC"
"-XX:-OmitStackTraceInFastThrow"
"-XX:+UnlockDiagnosticVMOptions" "-XX:+DebugNonSafepoints"]
:main-opts ["-e" "(load-file (str (System/getProperty \"user.home\") \"/.clojure/user.clj\"))"]}}}
And inside user.clj
I have this macro:
(defmacro perf-tools []
'(do
(require '[clj-async-profiler.core :as prof])
(require '[clj-java-decompiler.core :refer [decompile]])
(require '[clj-memory-meter.core :as mm])
(require '[criterium.core :as crit])
(.refer *ns* 'time+ #'user/time+)
(.refer *ns* 'heap #'user/heap)))
Whenever I want to do some performance work, I execute (user/perf-tools)
within the current namespace. The library code only then gets loaded (so I don't
wait extra to load it when the REPL starts), and it becomes available in the
current namespace as prof/...
, mm/...
, and also time+
and heap
without
any extra qualifiers.
That is all for today. All of this is pretty basic, but I spent some time reaching the setup I enjoy, so I hope this post can claim some of that time back for you.
load-file
in my config previously contained commas
instead of spaces, since commas in Clojure are treated as whitespace. This
approach is colloquially known as the "Corfield
comma."
The bug has been fixed; but the Corfield comma trick is still useful to be aware
of.↑