Clojure bites - dynamically add depencencies at runtime!

by FPSD — 2023-05-12


Overview

One pain point of REPL driven development is that even if it is possible to change an application while it is running, if you want to experiment with a new library it is need to add a new dependency to your project and restart the REPL to use it, possibly breaking your flow.

Fortunately a new set of functions to alleviate this pain are in the works and ready to be tested since Clojure 1.12.0-alpha2:

Current status

At the time of this post, add-lib is not yet in an official Clojure release but can be added as a development dependency, possibly putting it in an alias using for development; it does not make too much sense to have it bundled in your production build anyway.

Assuming that you have a `:dev/repl` alias in your deps.edn file, you can get this new functionality by adding version 1.12.0-alpha2 of Clojure to your deps:

  {:dev/repl {:extra-deps {org.clojure/clojure {:mvn/version "1.12.0-alpha2"}}}

add-lib in action

As an example I am going to re-write the FizzBuzz example from a previous post to use pattern matching instead of cond.

Initial setup

First step is to setup deps.edn to use Clojure 1.12.0-alpha2 in a development alias, it is a best practice to have development namespaces added to the source path, used to call add-lib, in order to not ship it with the production release.

Another good habit is to have a custom user namespace with code used during development, usually storing its code in dev/user.clj; to make it available in the repl, the "dev" directory must be added to :extra-deps vector.

Add the following map to the :aliases section of deps.edn map

  {:dev/repl {:extra-deps {org.clojure/clojure {:mvn/version "1.12.0-alpha2"}}
              :extra-paths ["dev"]}}

Put the following content in dev/user.clj

  (ns user
    (:require [clojure.repl.deps :refer [add-lib]]))

Scenario

For the sake of argument assume that we want to write a new implementation of FizzBuzz, in order to impress future recruiters.

Looking around we find out that pattern matching is a interesting approach to conditionals compared to ifs and conds, commonly used in functional programming languages (for ex. Erlang, Elixir and more) but also being implemented in more traditional languages like Python (as of Python 3.10).

Changing FizzBuzz to use pattern matching

Ready to approach our new mission we fire up our IDE, load the FizzBuzz project and start a repl, maybe running tests again to ensure that nothing broke while we were not watching.

Looking around we find core.match and, wow, the first example is an implementation of FizzBuzz, how lucky!

  (require '[clojure.core.match :refer [match]])
  
  (doseq [n (range 1 101)]
    (println
      (match [(mod n 3) (mod n 5)]
        [0 0] "FizzBuzz"
        [0 _] "Fizz"
        [_ 0] "Buzz"
        :else n)))

Two things to notice here:

Instead of adding the dependency to deps.edn and restarting the repl, we can download this dependency just for this session using add-lib.

From user namespace we can evaluate the following form to download the new library to try it out:

  (add-lib 'org.clojure/core.match)

At this point the repl will show all the depencencies being downloaded to satisfy our request. Once done it will possible to require the new library and use it:

  (require '[clojure.core.match :refer [match]])

  (match [true false]
    [_ true]     :case1
    [false _]    :case2
    [true false] :case3) ;; => :case3

Beautiful! We have added a new lib to our running repl, we have tested it and we are ready to use it to solve our problem.

Here is the new fizzbuzz implementation using core.match

  (defn fizzbuzz [n]
    (match [(mod n 3) (mod n 5)]
      [0 0] "FizzBuzz"
      [0 _] "Fizz"
      [_ 0] "Buzz"
      :else n)

Does it work? Well there is a proper test suite so lets try it out…and it works!

test-passing

Closing words

It does not happen every day to want to try a new library when working on something but at times I really really wanted to have a new dependency being added to a running project, without losing the current state, context and focus. And it is extremely simple, so give it a try!

Further reading

In this post I skimmed trough a lot a topics to focus on the add-lib flow, so here are some resources worth reading if you want to dig deeper, and I highly suggest so!

Discuss

Looking forward your feedback!