This is intended to bridge the gap between language-focused tutorials and setting
up a project.
Prerequisites
An installation of Clojure including the clj tool as well as Leiningen.
Running code
The REPL
As soon as you have installed Clojure, you can start a REPL (read-eval-print-loop)
session.
examples/clojure$ clj
user=> (+ 1 1)
2
Before clj there was (and still is) the clojure command. Typically one used to
call it with rlwrap as in rlwrap clojure to have a command line history via the
arrow up button. Now this is done just calling clj.
For more features, for example multiline editing, one can also use rebel-readline. It is also possible to run a REPL within the context of a text-editor, such that one can edit the code exactly like one does one’s source files. For VSCode for example there exists Calva.
Scripting
The simplest way to run multiple lines of code is to write them into a file and execute that file from the command line. This can basically be used in scripting tasks, as opposed to proper projects.
(prn (+ 1 1))
Run this as a script with
examples/clojure$ clj -M add_1.clj
2
One can also load a script into a REPL session.
(def add +)
It can then be accessed like this:
examples/clojure$ clj
user=> (clojure.main/load-script "add_2.clj")
user=> (add 2 2)
4
Depending on the script and your workflow, this may or may not have advantages. In any case, after making changes to the script, it can be reloaded by again entering (clojure.main/load-script "add_2.clj"). Thus any defined-and-then-changed function gets redefined and the new functionality can then be used within the ongoing session.
Altough one can read other Clojure scripts from Clojure script files transitively, i.e. one file may itself call load-script to invoke another file, this approach has certainly its limits.
This leads us finally to the question of how to set up projects in Clojure. Pleasantly doing this in Clojure is: very very easy.
Minimal projects
As soon as you want to distribute code across multiple source files,
you can very easily set up projects, following very limited conventions.
The first of these is creating source files within a src subdirectory.
The second one is having a correspondence between file names and namespace declarations
at the beginning of the files. More on namespaces is to be said in section after this one. Here
it should mean for us just file or context or module. But first let’s look at an example.
examples/clojure/greeter_1/src/greeter.clj:
(ns greeter)
(defn greet [name]
(prn (str "Hello, " name "!")))
examples/clojure/greeter_1/src/hello.clj:
(ns hello
(:require greeter))
(defn -main [& args]
(greeter/greet (first args)))
Run it
examples/clojure/greeter_1$ clj -M -m hello Daniel
"Hello, Daniel!"
Adhering to these conventions one can access the application from the REPL.
examples/clojure/greeter_1$ clj
user=> (require 'greeter) ; the syntax differs slightly from how it is used in a file
user=> (greeter/greet "Daniel")
"Hello, Daniel!"
In this case the -main function does not get executed. But now we can play around
with the application interactively. If we change something one of the files, we
need to reload the corresponding namespace.
user=> (require 'greeter :reload)
If the namespace to be loaded depends on another namespace (let’s say greeter depended on helper), and that one (i.e. helper) got changed, we can
transitively reload via
user=> (require 'greeter :reload-all)
After that, another function call to greeter/greet should reflect possible changes made to the function.
We have yet another option to call our greet function, namely to jump into the greeter namespace
and execute it from there.
user=> (in-ns 'greeter)
greeter=> (greet "Daniel")
Note that a (require 'greeter) call is necessary before we are able to
do this. If we don’t, the function call we intend to do will not work.
So that is what the user=> prompt is about. It shows us that when we open the REPL we operate
in the user namespace. One thing that is very useful to know is that we can create a namespace file
src/user.clj (containing the usual (ns user) namespace declaration at the beginning) which may contain
some arbitrary code which gets automatically executed
when the REPL is started. Since the code can consist of function definitions as well as some function call on
the top level, this is ideal for some initialization of the REPL-session that you may wish to perfom.
More on namespaces
Clojure namespaces correspond to Java namespaces, such that the file hierarchy
aligns with the namespace names. In the next example greeter is located one level below
from where it was in the last example.
examples/clojure/greeter_2/src/greeter/greeter.clj:
(ns greeter.greeter)
(defn greet [name]
(prn (str "Hello, " name "!")))
examples/clojure/greeter_2/src/hello.clj:
(ns hello
(:require greeter.greeter))
(defn -main [& args]
(greeter.greeter/greet (first args)))
Inside the REPL one can access it then.
examples/clojure/greeter_2$ clj
user=> (require 'greeter.greeter)
user=> (greeter.greeter/greet "Daniel")
"Hello, Daniel!"
Note that when using namespaces consisting of multiple segments, i.e. the-greeter,
the namespace declaration would be (ns the-greeter) (kebap-case) but the file name would be the_greeter.clj (snake case).
If not in the first examples, at least by now it would be understandable if you are irritated
by the long prefix to the greet function call. But this is easily treated. If we use
(ns hello
(:require [greeter.greeter :as g]))
or respectively
user=> (require '[greeter.greeter :as g]))
then we can call the function like this
(g/greet "Daniel")
This also works for the shorter example where greeter was not yet in the subdirectory ((require [greeter :as g])).
Note that greeter.greeter and [greeter.greeter :as g] are two forms to require a single dependency for
use within another namespace. require can take multiple of those entries. For example
(ns hello
(:require a-namespace
[another-namespace :as ans]))
For more involved usages see appendix.
Minimalistic dependency management
The first build tool you should check out is deps.edn.
It comes as part of the language and allows you to use dependencies,
from maven, from github, as well
as on the local file system.
Local dependencies
The files from the last example can be arranged such that the greeter itself will become a library
and the calling code an application using that library.
examples/clojure/deps_greeter/application/deps.edn:
{:deps
{greeter/greeter {:local/root "../library"}}}
examples/clojure/deps_greeter/library/deps.edn:
{}
examples/clojure/deps_greeter/library/src/greeter.clj:
(ns greeter)
(defn greet [name]
(prn (str "Hello, " name "!")))
examples/clojure/deps_greeter/application/src/hello.clj:
(ns hello
(:require [greeter :refer :all]))
(defn -main [& args]
(greet (first args)))
Run it with
examples/clojure/deps_greeter/application$ clj -m hello Daniel
"Hello, Daniel!"
Again, we can “reach” inside the application using the REPL.
examples/clojure/deps_greeter/application$ clj
Clojure 1.9.0
user=> (require '[greeter :refer :all])
nil
user=> (greet "Daniel")
"Hello, Daniel!"
nil
Dependencies from external repositories
This of course does work not only for local libraries,
but for dependencies from github and maven as well.
examples/clojure/deps/deps.edn:
{:deps {org.clojure/java.classpath {:mvn/version "1.0.0"}}}
examples/clojure/deps$ clj
Clojure 1.9.0
user=> (require '[clojure.java.classpath :refer :all])
nil
user=> (system-classpath)
[Shows classpath info]
Working with local dependencies is great, because code changes are directly
available, like when you have the code in a separate namespace. Yet it already is
in a form where it can be made a ‘external’ github dependency by just changing
from :local/root to :git/url (see next paragraph for an example).
Using a library for automatic code reloading
While we are at it, we can now show to improve our REPL-based development workflow
insofar as we can avoid having to reload manually after changing our artifacts. This
is done by using the clj-reloader library.
{:deps {thiru/clj-reloader {:git/url "https://github.com/thiru/clj-reloader"
:sha "8646987342602ab2c6fd0b966386eacda0844f64"}}
We learned earlier about the user namespace. Here we can use that to our advantage
and create a file src/user.clj, whose code gets executed as soon as we enter the REPL.
(ns user
(:require [reloader.core :as reloader]))
(reloader/start ["src"])
As a consequence, changing an artifact and saving the file lets us immediately re-run
possibly redefined functions within our ongoing REPL-session. Isn’t that great?
Minimalistic testing
Using the deps tool you can install a test runner, which
facilitates writing unit tests with clojure.test,
which is also part of the language.
examples/clojure/adder/deps.edn:
{
:deps {com.cognitect/test-runner {
:git/url "https://github.com/cognitect-labs/test-runner.git"
:sha "209b64504cb3bd3b99ecfec7937b358a879f55c1"}}
:aliases {:test {:extra-paths ["test"]
:main-opts ["-m" "cognitect.test-runner"]}}
}
examples/clojure/adder/src/adder.clj:
(ns adder)
(def add +)
examples/clojure/adder/test/adder_test.clj:
(ns adder-test
(:require [clojure.test :refer :all]
[adder :refer :all]))
(deftest test-adder
(is (= 2 (add 1 1))))
Execute with
examples/clojure/adder$ clj -Atest
Running tests in #{"test"}
Testing adder-test
Ran 1 tests containing 1 assertions.
0 failures, 0 errors.
With the Cognitect test runner
test suites in the form of namespaces (separate namespace segments
after -n with . as usual if there are any) can be executed by
clj -Atest -nadder-test
and single tests with (separate test name from namespace with /)
clj -Atest -vadder-test/test-adder
Leiningen
While the tools that come with Clojure work just fine for many use cases, there may be more complex
use cases where one would consider build tools with more features. One such build tool is Leiningen (or lein for short). There are other tools to, but Leiningen is widely used and has been around for a long time. We will give a quick overview of how it is set up with regards to our greeter examples.
And while we are at it, we will use the opportunity to show how Java can be mixed in when using Leiningen.
examples/clojure/lein_greeter/src/clj/hello.clj:
(ns hello
(:import Greeter))
(defn -main [& args]
(Greeter/greet (first args)))
examples/clojure/lein_greeter/src/java/Greeter.java:
public class Greeter {
public static void greet(String name) {
System.out.println("Hello, " + name + "!");
}
}
examples/clojure/lein_greeter/project.clj:
(defproject lein-greeter "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.10.0"]]
:main ^:skip-aot hello
:source-paths ["src/clj"]
:java-source-paths ["src/java"])
Run it with
examples/clojure/lein_greeter$ lein run Daniel
Hello, Daniel!
Tests
The src and test code is the same as in the the adder example.
See
examples/clojure/lein_adder/src/adder.clj
and
examples/clojure/lein_adder/test/adder_test.clj.
The Leiningen project description includes the test path as an additional source path.
examples/clojure/lein_adder/project.clj:
(defproject lein-adder "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.10.0"]]
:source-paths ["src" "test"])
Run tests
examples/clojure/lein_adder$ lein test
lein test adder-test
Ran 1 tests containing 1 assertions.
0 failures, 0 errors.
Test a single namespace
examples/clojure/lein_adder$ lein test :only adder-test
or execute a single test
examples/clojure/lein_adder$ lein test :only adder-test/test-adder
Appendix
require, use, import
These are used to invoke classes/namespaces.
import, use
Because most of the time you will encounter require, let us start with import and use to get
it out of the way.
The import call is used with Java classes.
(ns hello
(:import Greeter))
Then there is use, which is a combination of require and refer (see below), but
from what I’ve read is discouraged in favour of using the latter.
require
We have seen different calls to require depending on if it was part
of a namespace declaration as in
(ns hello
(:require [greeter :refer :all]))
or when called in the REPL
user=> (require '[greeter :refer :all])
Let’s quickly go trough some examples, just to get some rough sense for
how it is used under different circumstances.
Although this is discouraged, one can import all symbols from one namespace into
the current one
(ns hello
(:require [greeter :refer :all]))
such that we wouldn’t have to call (greeter/greet "Daniel") but rather (greet "Daniel").
To avoid namespace clashes one can explicitely refer to certain functions.
(ns hello
(:require [greeter :refer [greet]]))
(greet "Daniel")
Or one just uses - as is commonly done - shorthands for imported namespaces.
(ns hello
(:require [greeter :as g]))
(g/greet "Daniel")
Both work in combination, too, such that only certain functions are available.
(ns hello
(:require [greeter :refer [greet] :as g]))
(g/greet "Daniel")
One can even rename a function like this
(ns hello
(:require [greeter :as g :refer [greet] :rename {greet gr}]))
(g/gr "Daniel")