Clojure and Exercism
I’ve been trying to learn Clojure (a LISP) through Exercism, a programming exercises tool. It took me an hour to get Hello, World! up and running, so I thought I’d document how it’s done. I’m using Leiningen on Mac OS 10.11.4.
The Installing Clojure page on Exercism details how to install Leiningen; that part is easy.
Installing exercism
is likewise easy, so we run exercism fetch clojure hello-world
.
And then we enter a world of pain.
exercism
downloads a project structure:
hello-world/
-- project.clj
-- README.md
-- test/
-- hello_world_test.clj
The README helpfully tells us what Hello, World! is, and a specification for the answer.
How are we to come up with our answer?
lein
gives access to a REPL we can use to write an answer, but there’s no indication of
where to put our files so that lein
can see them.
Let’s run lein test
to see what lein
complains about.
Exception in thread "main" java.io.FileNotFoundException:
Could not locate hello_world__init.class or hello_world.clj on classpath.
Please check that namespaces with dashes use underscores in the Clojure file name.,
compiling:(hello_world_test.clj:1:1)
Fine. It’s looking for hello_world.clj
. Let’s make one!
I’ve put the following in hello-world/hello_world.clj
:
(defn hello
[]
"Hello, World!"
[name]
(str "Hello, " name "!"))
(defn main- [& _] (println "Hello!"))
lein test
fails again, with the same error.
Do we get any hints from the test file? It starts with a namespace declaration:
(ns hello-world-test
(:require [clojure.test :refer [deftest is]]
hello-world))
We’re going to want a hello-world
namespace, so let’s put that at the top of our hello_world.clj
.
(ns hello-world)
Still fails with the same error.
OK, the thing that is telling lein
what to do must be project.clj
, and it turns out to contain the following:
(defproject hello-world "0.1.0-SNAPSHOT"
:description "hello-world exercise."
:url "https://github.com/exercism/xclojure/tree/master/exercises/hello-world"
:dependencies [[org.clojure/clojure "1.8.0"]])
None of that tells lein
where to look for the source file.
If we make a new lein
project somewhere, let’s see what the project file is supposed to look like.
Go to a temporary directory and use lein new app newproj
.
The source tree looks like:
newproj/
-- CHANGELOG.md
-- LICENSE.md
-- README.md
-- doc/
-- intro.md
-- project.clj
-- resources/
-- src/
-- newproj/
-- core.clj
-- test/
-- newproj/
-- core_test.clj
And project.clj
looks like:
(defproject newproj "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.8.0"]]
:main ^:skip-aot newproj.core
:target-path "target/%s"
:profiles {:uberjar {:aot :all}})
The only interesting thing there seems to be :main ^:skip-aot newproj.core
.
Let’s try putting :main ^:skip-aot hello-world
into our own project.clj
.
lein test
continues to fail with the same error.
Looking up :skip-aot
, it just tells lein
to skip Ahead-Of-Time compilation, which isn’t what we want.
With a heavy heart, then, let’s restructure hello-world
so it looks exactly like newproj
:
hello-world/
-- README.md
-- project.clj
-- src/
-- hello_world/
-- hello_world.clj
-- test/
-- hello_world/
-- hello_world_test.clj
Miraculous! We have a different error!
Exception in thread "main" java.io.FileNotFoundException:
Could not locate hello_world_test__init.class or hello_world_test.clj on classpath.
I think this might be a back-step, because beforehand it was at least finding the test file.
I get the same error if I navigate into the test folder and run lein test
.
And if we try lein run
, we get the original error:
Exception in thread "main" java.io.FileNotFoundException:
Could not locate hello_world__init.class or hello_world.clj on classpath.
From the Leiningen documentation:
The
src/my_stuff/core.clj
file corresponds to themy-stuff.core
namespace.
That would imply that our source file corresponds to the hello-world.hello-world
namespace.
Let’s try flattening out the structure a bit, and returning the hello_world_test.clj
to where at least
lein
recognised it:
hello-world/
-- README.md
-- project.clj
-- src/
-- hello_world.clj
-- test/
-- hello_world_test.clj
And it works! Woohoo! (Well, the tests fail, but that’s because I’m new to Clojure and missed out a bunch of parentheses.)
The final contents of src/hello_world.clj
, causing the tests to pass, were:
(ns hello-world)
(defn hello
([] "Hello, World!")
([namevar] (str "Hello, " namevar "!")))