This is an example of a simple Clojure app written using Literate programming paradigm using org-mode.
First we setup the project. In the source block bellow you can see that the
language is set to zsh
. This tells babel that this snippet should be executed
within a shell. I also set the :results
header argument to silent because in
this case we don’t care about the output of running this snippet.
mkdir -p simple-app/src/simple_app
mkdir -p simple-app/test/simple_app
mkdir -p simple-app/dev
Now we create our basic deps file. Note that our source block name is set to clojure. We do this because that will make the file sintax hilighting work.
To save a snippet like this on a path we set the :tangle
header argument and
when we run the emacs command org-babel-tangle
our file will be saved to the
path specified.
{:deps
{org.clojure/clojure {:mvn/version "1.10.1"}}
:aliases
{:test
{:extra-paths ["test"]
:extra-deps {com.cognitect/test-runner
{:git/url "https://github.com/cognitect-labs/test-runner.git"
:sha "209b64504cb3bd3b99ecfec7937b358a879f55c1"}}
:main-opts ["-m" "cognitect.test-runner"]}}}
Now that we have our basic project setup we can work on our project namespaces.
First we start by starting a REPL. There are a few different ways you can
approach this. If you are in emacs you could simply run cider-jack-in
but
if you notice this file isn’t in the same directory as the project. That means
cider wont know what deps.edn
file to load.
A solution for this is to manually run your REPL command. Notice the :dir
header variable bellow. It tells Emacs the relative path of where to execute the
command. A quirk of doing this is that the following command is blocking and Emacs
seems to freeze on waiting for output. You can C-g
to unfreeze the editor.
Notice we set :session
so that our future zsh commands dont get blocked.
clojure -R:test -C:test -Sdeps '{:deps {nrepl {:mvn/version "0.7.0"} cider/cider-nrepl {:mvn/version "0.25.2"}}}' -m nrepl.cmdline --middleware '["cider.nrepl/cider-middleware"]'
Lets fetch that nrepl port.
cat .nrepl-port
Now we run cider-connect-clj
and we can start working on our project.
Lets start with a simple test.
(require '[clojure.test :refer [deftest testing is]])
(deftest a-test
(testing "FIXME, I fail."
(is (= 0 1))))
Since we are not in a typical dev environment there are a few different ways to
run clojure code. Notice that we need to set :epilogue
We could run them as if we were in the command line, but make sure you
org-babel-tangle
first. Notice that we set :epilogue
to “true”. This is a
weird quirk we have to do because the output returns to standard error so Emacs
thinks there is no output. We also set results as a pp so we can comfortably
collapse the results.
clj -A:test
Running tests in #{"test"} Testing simple-app.core-test FAIL in (a-test) (core_test.clj:6) FIXME, I fail. expected: (= 0 1) actual: (not (= 0 1)) Ran 1 tests containing 1 assertions. 1 failures, 0 errors.
Ideally, we should really try to evaluate code already directly on our REPL.
(run-tests 'simple-app.core-test)
Testing simple-app.core-test FAIL in (a-test) (NO_SOURCE_FILE:6) FIXME, I fail. expected: (= 0 1) actual: (not (= 0 1)) Ran 1 tests containing 1 assertions. 1 failures, 0 errors.
Notice that we set :results
to output. This tells Emacs to get the value from
standard out which is way more informative than the return value of the
run-tests
function. In this case the function returns a map of values like:
{:test 1, :pass 0, :fail 1, :error 0, :type :summary}
I started with tests to score some karma points but this technique really shines is in the ability to interactively evaluate code and see the results inline. So lets do more of that now.
(defn hello [subject]
(prn (str "Hello " subject)))
(hello "World")
"Hello World"
First of notice we are still setting results to “output”. This is because this
function prints to standard out. Thats not something new, but checkout the
:tangle
header. The value is set to a “scratch.clj” file in the dev directory.
That file will now become a bucket where all the one off commands I run will go
to. I thought this was a neat concept as I can go back to that file and just see
code int the order I meant to execute it without all the documentation.
Now lets play a bit more with that :results
header. We are going to se a few
examples here for mori information you can go to the official docs.
The default value for results is “value”. Wich plainly means the result of the evaluation will be printed in the documet. There is an anoying quirk (or bug?) that with clojure evaluation where the result is printed only from the first form. I dont mind this so much as the idea of this approach is to have documentation and evaluation step by step.
(str "foo")(+ 1)(prn "baz")
foo
If the evaluation returns something that looks like a list we can use
:results value list
["foo" "bar" "baz"]
- foo
- bar
- baz
Org will try to print things like sequences of sequences as tables by default
but it can be forced by setting :results value list
[["foo" "bar" "baz" "qux"]
["1" "2" "3" "4"]
[1 2 3 4]
'(:one :two :three :four)
(take 4 (range))]
foo | bar | baz | qux |
1 | 2 | 3 | 4 |
1 | 2 | 3 | 4 |
:one | :two | :three | :four |
0 | 1 | 2 | 3 |
I could not find a super clean way to print maps nicely. Fortunately clojure pprint has us covered.
(clojure.pprint/print-table [{:a 1 :b 2 :c 3} {:b 5 :a 7 :c "dog"}])
| :a | :b | :c | |----+----+-----| | 1 | 2 | 3 | | 7 | 5 | dog |
There is so much more you can do with the :results
header like printing
images, raw data, org code blocks, drawers, etc. Be shure to look at the docs
and play around on your own.
One last tip I want to share is that you might want to edit clojure blocks in
clojure mode. To achieve this there is a nifty org-edit-special
command. This
command will put your snippet in an Emacs pop-up in clojure mode.
(defn -main [target]
(hello target))
Run org-babel-tangle
and org-babel-execute-buffer
and profit.
clojure -m simple-app.core "Clojure North"
There you have it a fully running Clojure project in one file.