Clojure digressions - Setting up a playground

by FPSD — 2023-06-23


Disclaimer

Why a new series named Clojure Digressions?

Clojure Bites is more suited for short introductions to Clojure related libraries and tools, instead with digressions I'd like to talk about more general topics, like development environment setup, good/bad habits and personal development as a Clojure developer.

Like a conversation ice breaker, I start by talking about my personal take on some specific argument, looking forward to get input from other, possibly more experienced, developers on some specific topic.

Overview

A playground is a safe place aimed at experimentation, without any legacy background limiting your fantasy and willingness to explore solutions outside your usual comfort zone i.e. all the code you have already written for your company/product.

It is no secret that as professional developers we are expected to reuse as much as possible of what we have at hand in our code base, being careful when adding new dependencies, limit experimentation to when it is really really needed to avoid introducing unforeseen costs but at the same time we love to try out new things! What if we had a safe place to experiment, just taking out a small bit of our business domain and test it against a different approach?

Starting from scratch

You want to quickly sketch out something, no REPL is running, the first thing you can do is to fire up an interactive REPL by running $ clojure or, if you want readline support, $ clj; now you have a prompt where you con evaluate forms using the rich clojure.core library. Nothing too advanced, the same is available with the default Python, Ruby and many other prompts. But it is still something.

Lets spice up our REPL by replacing the standard readline lib with rebel that will give us autocompletion, hints and much more. To try it out quickly:

$ clojure -Sdeps "{:deps {com.bhauman/rebel-readline {:mvn/version \"0.1.4\"}}}" -m rebel-readline.main

The `-Sdeps` is used to pull in more dependencies, the `-m` parameter is used to specify an entry point to be called; this will initialize a REPL session using rebel readline interface. Learn more here.

If you'll like it you can create an alias in your `~/.clojure/deps.edn` file in order to be able to start a rebel REPL without being forced to remember all the required parameters, here is an example:

{
  :aliases {:rebel {:extra-deps {com.bhauman/rebel-readline {:mvn/version "0.1.4"}}
                    :main-opts  ["-m" "rebel-readline.main"]}}
}

And invoke it simply with:

$ clojure -M:rebel

The asciinema of the official docs is worth one billion words ;)

Beyond throw away sessions

Working directly in the REPL prompt is not really ergonomic in some cases and you may prefer the comfort of your favorite IDE or editor to hack on some code; the first step is to start a REPL to which you can attach to. As with rebel readline it not required to to have an alias in your $HOME/.clojure/deps.edn or project's deps.edn:

$ clojure -Sdeps "{:deps {nrepl/nrepl {:mvn/version \"1.0.0\"}}}" -m nrepl.cmdline

nREPL server started on port 39435 on host localhost - nrepl://localhost:39435

Now you can connect your IDE to the REPL and start hacking! Please notice that the port may change at each invocation. Deciding what to work on is left as an exercise to the reader :)

Like we did for rebel readline, we can add an alias to our $HOME/.clojure/deps.edn file to make it easier in future to start a REPL we can attach to:

{
  :aliases {:rebel {:extra-deps {com.bhauman/rebel-readline {:mvn/version "0.1.4"}}
                    :main-opts  ["-m" "rebel-readline.main"]}
            :nrep  {:extra-deps {nrepl/nrepl {:mvn/version "1.0.0"}}
                    :main-opts  ["-m" "nrepl.cmdline"]}}
}

Starting it with:

$ clojure -M:nrepl

At this point I encourage you to test this setup with your own IDE to get a real feel of it.

Some more spices

Being able to run a REPL on demand is already something useful but we can get more out of it, for example being able to pull in new dependencies on demand in the currently running process using the upcoming add-lib in the repl.deps namespace, see here for more details.

Add a new alias to our clojure deps file:

{
  :aliases {:rebel {:extra-deps {com.bhauman/rebel-readline {:mvn/version "0.1.4"}}
                    :main-opts  ["-m" "rebel-readline.main"]}
            :add-libs {:extra-deps {clojure.org/clojure {:mvn/version "1.12.0-alpha3"}}}
            :nrep  {:extra-deps {nrepl/nrepl {:mvn/version "1.0.0"}}
                    :main-opts  ["-m" "nrepl.cmdline"]}}
}

Now we can add this capability to either rebel or nrepl alias, for example, running:

$ clojure -M:add-libs:rebel

will start a new REPL pulling in both the new Clojure version with support to dynamically load libraries and a rebel readline prompt; the same would apply if replacing rebel with nrepl, i.e. we would have a nrepl session with the add-libs support.

It is useful to be able to combine more functionalities coming from different aliases when starting a new REPL process. Again I encourage everyone to play with it and come with a setup the covers your requirements.

Other useful tools worth considering for your personal toolbox:

It can be seen as a safe place to play with unknown possible solutions, in an isolated environment, collect useful information and then decide if we can introduce them to our daily stack.

How can it be useful?

Which tools should I include in mine?

Is there something obvious that I am missing?

Are we playground yet?

We have learned how to setup an interactive or a nREPL to connect to from an IDE and we have added some useful tools to improve our developer experience but where do we put the results of our experiments? I usually start a new project each time I want try out something and honestly it is a straight forward process especially if using tools like clj-new, taking from the project README:

# one-off to install clj-new as a tool:
$ clojure -Ttools install com.github.seancorfield/clj-new '{:git/tag "v1.2.399"}' :as clj-new

# commands to create new projects:

# create a new app:
$ clojure -Tclj-new app :name myname/myapp
# create a new library:
$ clojure -Tclj-new lib :name myname/mylib
# create a new template:
$ clojure -Tclj-new template :name myname/mytemplate
# create a new project from a public template:
$ clojure -Tclj-new create :template electron-app :name myname/myelectron-app#+END_SRC

Recently maybe I have created too many throw away projects and creating a full fledged project structure each time feels like a waste of time; ideally I'd like to open a new file, connect to a fully capable REPL (or start a new one) and start hacking. When finished commit the new file or changes to existing ones and move one.

One advantage I see with this approach is that I can keep all these small projects in one place, accumulating knowledge that can be easily reused later. An added benefit is that it is easy to add another powerful tool like Clerk to generate playbooks to interact with and document the code; two months from now you may want to get back to your experiment and easily get back to it and its conclusions without spending too much time.

Conclusion

We started from a simple REPL, added some tasty juice to it and ended up creating a safe place to play and experiment. I want to point out that this is just my personal take and not a default in the Clojure community (or if that is, it is just a coincidence).

I am curious to learn how others are addressing the same problem, if it is a problem anyway.

Discuss