Before we can create awesome-sauce with Clojure, we need an environment set up. The fastest way to get there on Windows is by using choco. Simply run the two commands below with their following wizards and you should be good.
iwr https://chocolatey.org/install.ps1 -UseBasicParsing | iex
choco install lein
When installing Leiningen, we also get the =JVM= which is required to get any serious work done in Clojure. At this point you could run
lein repl
And start interacting with Clojure through a Read-Eval-Print-Loop.
I tend to say Clojure, but often times what I actually mean is
ClojureScript. The difference is that Clojure run on the Java Virtual
Machine or JVM in short, whilst ClojureScript run in a
JavaScript-Engine. There are very few differences between the two
languages though, so you can mostly run your code in both lands. You
can do so by using the file-extension cljc
, which stands for
Clojure-Common. One advantage of doing so, is that you can run all of
your test-passes on the JVM with somewhat better performance than you would
get, say in a browser. For this session, we will stick to the regular
cljs-format for the sake of simplicity. We will also be using
figwheel
which will hot-load our code while keeping our state in-tact
and Re-frame
which is a small framework for state-handling that
depends on Reagent
which is a thin layer on-top of React
.
At this point of the session, we will boot up figwheel
and touch upon
some code.
lein figwheel dev
I’ve strapped on the nightlight
editor for convenience, so
we can just get started without tweaking any particular IDE.
lein nightlight --port 4000
It’s idiomatic clojure, to name your starting-point core
. This is
where you bring in the thunk of all your dependency-branches and
usually where you initialize the application itself.
(ns koans.core
(:require [reagent.core :as reagent]
[re-frame.core :as re-frame]
[koans.events]
[koans.subs]
[koans.routes :as routes]
[koans.views :as views]
[re-frisk.core :refer [enable-re-frisk!]]
[nightlight.repl-server]))
(def debug?
^boolean js/goog.DEBUG)
(defn dev-setup []
(when debug?
(enable-console-print!)
(enable-re-frisk!)
(println "dev mode")))
(defn mount-root []
(re-frame/clear-subscription-cache!)
(reagent/render [views/main-panel]
(.getElementById js/document "app")))
(defn ^:export init []
(routes/app-routes)
(re-frame/dispatch-sync [:initialize-db])
(dev-setup)
(mount-root))
For this particular application, we don’t really need much initial data, but if we did, this would be the place to put it. It’s also the place I would do validation of that data, if any.
(ns koans.state
(:require [clojure.test.check.generators :as gen]
[clojure.spec :as s]))
(def default-state
{:assertions {}
:data-structures {}
:lazy-sequences {}
:threading-macros {}})
Again, this is a very crude SPA, so there’s really no routing going on, but you can kind of see how you would model it from the code below.
(ns koans.routes
(:require-macros [secretary.core :refer [defroute]])
(:import goog.History)
(:require [secretary.core :as secretary]
[goog.events :as events]
[goog.history.EventType :as EventType]
[re-frame.core :as re-frame]))
(defn hook-browser-navigation! []
(doto (History.)
(events/listen
EventType/NAVIGATE
(fn [event]
(secretary/dispatch! (.-token event))))
(.setEnabled true)))
(defn app-routes []
(secretary/set-config! :prefix "#")
(defroute "/" []
(re-frame/dispatch [:set-active-panel :koans-panel]))
(hook-browser-navigation!))
In every Clojure project to this point, I’ve had to use some general utility-functions that’s not included in the “Std-lib”, so I tend to start every project with a utils-file.
(ns koans.utils
(:require [reagent.core :as reagent]
[cljsjs.react-highlight]))
(defn log [& args]
(do (apply js/console.log args)
args))
(def highlight
(reagent/adapt-react-class js/Highlight))
(ns koans.views
(:require [re-frame.core :as re-frame :refer [subscribe dispatch]]
[koans.koans :as koans]
[koans.utils :as u]))
(defn- footer []
[:footer.flex.row.align-center.justify-center
(str "It's on github ")
[:a {:href "//github.com/bdo-labs/koans"} [:i.ion-social-github]]
(str " go grab it :)")])
(defn- koans-panel []
(let [percent-completed (subscribe [:percent-completed])]
(fn []
[:div.container
[:div.completed {:style {:width (str @percent-completed "%")}}]
[:header.flex.column
[:div.text-width
[:h1 "Clojure " [:strong "Koans"]]
[:p (str "A koan is a riddle or puzzle that Zen Buddhists use during "
"meditation to help them unravel greater truths about the world"
" and about themselves. Zen masters have been testing their"
" students with these stories, questions, or phrases for centuries.")]
[:p (str "For quick-reference to how these Koans are solved, I recommend having a look at ")
[:a {:href "//cljs.info"} "cljs.info"]]
[:em (str "Ohh! And just replace `:_` with whatever value you think is correct")]]]
[koans/assertions]
[koans/data-structures]
[koans/lazy-sequences]
[koans/threading-macros]
[footer]])))
;; main
(defn- panels [panel-name]
(case panel-name
:koans-panel [koans-panel]
[:div]))
(defn main-panel []
(let [active-panel (subscribe [:active-panel])]
(fn []
[panels @active-panel])))
The Koans themselves along with some ClojureScript inception. You can see that we compile ClojureScript-code from ClojureScript ;)
(ns koans.koans
(:require [re-frame.core :as re-frame :refer [subscribe dispatch]]
[reagent.core :as reagent]
[cljs.js :as cljs]
[koans.utils :as u]))
(defonce compiler-state
(cljs/empty-state))
(defn- eval [input]
(cljs/eval-str compiler-state (str input) nil
{:eval cljs/js-eval} #(:value %)))
(defn- koan [intro code assert]
(fn [intro code]
(let [v (if-not (re-matches #"_" (str code)) (eval code) false)]
(assert intro v)
[:div.koan
[:p [:small intro]]
[:div.code {:class (if v "success" "")}
[u/highlight {:language "clojure"} (str code)]
;; [:pre [:code (pr-str code)]]
[:pre (str "=> " v)]]])))
(defn assertions []
(let [completed? @(subscribe [:assertions-completed?])
assert #(dispatch [:assertion %1 %2])]
[:div.card
[:h3 "Assertions"]
[:i.ion-checkmark-round.checkmark {:class (if completed? "succeeded" "failed")}]
[:p (str "Clojure is a dialect of lisp and in lisp we mostly work with lists."
" A list is defined using parenthesis and if the first element of the"
" list is a symbol, the list will be evaluated as a function."
" In the assertions below you can see this in action. In the first assertion"
" `=` is the function-name and the following elements of the list is"
" it's arguments")]
[koan (str "Only real truths") '(= :_ true) assert]
[koan (str "Same for falsehoods") '(= :_ false) assert]
[koan (str "More than the first") '(> :_ 1) assert]
[koan (str "You can compare two of the same type") '(= :_ (keyword "bar")) assert]]))
(defn data-structures []
(let [completed? @(subscribe [:data-structures-completed?])
assert #(dispatch [:data-structure %1 %2])]
[:div.card
[:h3 "Data-structures"]
[:i.ion-checkmark-round.checkmark {:class (if completed? "succeeded" "failed")}]
[:p (str "As mentioned, lists are very common in Clojure. But there are many "
"other valuable data-structures you can play with.")]
[:div
[koan (str "Vectors are formed using square-brackets. [:a :b :c :d]") '(= :_ (first [1 1 2 2 3 3 4 4 5 5])) assert]
[koan (str "Maps are formed using curly-braces. {:hello \"world\" :foo \"bar\"}") '(= :_ (:b {:a 1 :b 2 :c 3})) assert]
[:small [:em (str "Note that maps need to be balanced")]]
[koan (str "Sets are formed by prefixing curly-braces with a hash. #{:a 1 :b :c 3}") '(contains? #{1 2 3 4 5} :_) assert]
[:small [:em (str "Sets always contain unique values")]]]
[:div
[:p (str "These are all collections and collections share a bit of API that's"
" used for testing/verification and some modification."
" Although most manipulation is specific to each data-type.")]
[:p (str "Sequential data-structures are called Seq in Clojure, these share even more API.")]]]))
(defn lazy-sequences []
(let [completed? @(subscribe [:lazy-sequences-completed?])
assert #(dispatch [:lazy-sequence %1 %2])]
[:div.card
[:h3 "Lazy Sequences"]
[:i.ion-checkmark-round.checkmark {:class (if completed? "succeeded" "failed")}]
[:p (str "Languages are often separated into categories of lazy or eager evaluation. "
"As you might have guessed, Clojure falls into the latter. "
"What that means, is we can have data-structures that contain an endless amount "
"of items, but only the ones we observe are taken into account.")]
[:div
[koan (str "We can make small sequences") '(= :_ (take 2 (range 3))) assert]
[koan (str "Or huge at the same cost") '(= :_ (take 2 (range 999999999999))) assert]]
[:div
[koan (str "They can also be a product of our own structures") '(= :_ (nth (cycle [:a :b :c]) 3)) assert]]]))
(defn threading-macros []
(let [completed? @(subscribe [:threading-macros-completed?])
assert #(dispatch [:threading-macro %1 %2])]
[:div.card
[:h3 "Threading Macros"]
[:i.ion-checkmark-round.checkmark {:class (if completed? "succeeded" "failed")}]
[:p (str "Threading is an eloquent solution to making code more readable."
"It's API is also fairly similar to that of transducers, so we can "
"in many cases also gain heavily in performance. Yay!")]
[:div
[koan (str "Thread first -> will direct the output of fn to the first argument of the next fn")
'(= :_ (-> [:a :b :c :d] (nth 1))) assert]
[koan (str "Thread last ->> will direct the output of fn to the last argument of the next fn")
'(= :_ (->> [:a :b :c :d] (mapv name))) assert]
]
[:div
[:p (str "These two are the most commonly used threading-macros. Clojure also "
"has a few special ones for other common use-cases. These include"
"some-> some->> as-> cond-> cond->>")]
[:p (str "I'm not going to cover transducers in-depth, but feel free to ask ;)")]]]))
(ns koans.events
(:require [re-frame.core :as re-frame :refer [trim-v reg-event-db reg-event-fx]]
[koans.state :as state]))
(def interceptors
[trim-v])
(reg-event-db
:initialize-db
(fn [_ _]
state/default-state))
(reg-event-db
:set-active-panel
[interceptors]
(fn [db [active-panel]]
(assoc db :active-panel active-panel)))
(reg-event-db
:assertion
[interceptors]
(fn [db [k v]]
(assoc-in db [:assertions k] v)))
(reg-event-db
:data-structure
[interceptors]
(fn [db [k v]]
(assoc-in db [:data-structures k] v)))
(reg-event-db
:threading-macro
[interceptors]
(fn [db [k v]]
(assoc-in db [:threading-macros k] v)))
(reg-event-db
:lazy-sequence
[interceptors]
(fn [db [k v]]
(assoc-in db [:lazy-sequences k] v)))
(ns koans.subs
(:require [re-frame.core :as re-frame :refer [reg-sub]]
[clojure.spec :as s]
[koans.utils :as u]))
(reg-sub
:active-panel
(fn [db]
(:active-panel db)))
;; Assertions
(reg-sub
:assertions
(fn [db]
(vals (:assertions db))))
(reg-sub
:assertions-completed?
:<- [:assertions]
(fn [assertions]
(empty? (remove true? assertions))))
;; Data-structures
(reg-sub
:data-structures
(fn [db]
(vals (:data-structures db))))
(reg-sub
:data-structures-completed?
:<- [:data-structures]
(fn [data-structures]
(empty? (remove true? data-structures))))
;; Threading-macros
(reg-sub
:threading-macros
(fn [db]
(vals (:threading-macros db))))
(reg-sub
:threading-macros-completed?
:<- [:threading-macros]
(fn [threading-macros]
(empty? (remove true? threading-macros))))
;; lazy-sequences
(reg-sub
:lazy-sequences
(fn [db]
(vals (:lazy-sequences db))))
(reg-sub
:lazy-sequences-completed?
:<- [:lazy-sequences]
(fn [lazy-sequences]
(empty? (remove true? lazy-sequences))))
;; Stats
(reg-sub
:koans
:<- [:assertions]
:<- [:data-structures]
(fn [& koans]
(->> koans
(flatten)
(remove #(not (boolean? %))))))
(reg-sub
:completed-koans
:<- [:koans]
(fn [koans]
(remove false? koans)))
(reg-sub
:percent-completed
:<- [:koans]
:<- [:completed-koans]
(fn [[total-koans completed-koans]]
(int (* (/ (count completed-koans) (count total-koans)) 100))))
I typically use garden, but I’ve chosen straight up css for now, to keep it simple. But feel free to read up on garden, it’s not just a pre-processor, it gives you the complete power of clojure in your styles-sheets!
@import "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.10.0/styles/github.min.css";
/* Base */
html,
body,
#app {
width: 100%;
height: 100%;
}
html {
font-family: Open sans, Helvetica, Sans serif;
font-size: 62.5%;
}
body {
font-size: 1.8em;
font-weight: 100;
margin: 0;
overflow: hidden;
}
h1,
h2 {
font-weight: 100;
}
a {
color: rgb(254,197,52);
}
.text-width {
max-width: 70rem;
}
i[class^=ion] {
color: rgb(254,197,52);
font-size: 5rem;
margin: 1rem;
}
.checkmark {
position: absolute;
right: -8rem;
top: 3rem;
transition: .5s ease;
}
.failed {
transform: scale(0);
opacity: 0;
}
.succeeded {
transform: scale(1);
opacity: 1;
}
.card {
box-sizing: border-box;
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.3);
margin: 4rem;
max-width: 80rem;
position: relative;
}
.container {
height: 100%;
max-width: 100rem;
margin: 0 auto;
padding-left: 2rem;
padding-right: 2rem;
text-align: left;
}
.completed {
background: rgb(254,197,52);
position: fixed;
top: 0;
left: 0;
height: 0.5rem;
z-index: 10;
transition: 1s ease;
}
.koan {
margin-top: 2em;
}
.code {
background-color: rgb(205,235,250);
border-radius: 0.4rem;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
overflow: hidden;
transition: background-color .5s ease;
}
.code code.hljs {
background-color: transparent;
}
.code.success {
background-color: rgb(205,250,235);
}
.code pre {
margin: 0;
padding: 0.5rem 1rem;
}
.code pre + pre {
background: rgba(255,255,255,0.5);
font-size: 0.7em;
}
footer {
padding: 4rem;
text-align: center;
}
/* Utility-classes */
.flex {
display: flex;
flex-wrap: wrap;
flex-direction: row;
}
.flex.no-wrap {
flex-wrap: none;
}
.flex.column {
flex-direction: column;
}
.fill {
flex: 1;
}
.fill-1 {
flex: 1;
}
.fill-3 {
flex: 2;
}
.fill-3 {
flex: 3;
}
.align-center {
align-items: center;
}
.align-stretch {
align-items: stretch;
}
.justify-center {
justify-content: center;
}
.justify-space-between {
justify-content: space-between;
}
/* Theme */
#app {
background: linear-gradient(135deg,rgb(100,175,245), rgb(47,63,224));
background-attachment: fixed;
overflow: auto;
color: white;
text-align: center;
}
#app > div {
margin-top: 2rem;
margin-bottom: 8rem;
}
.card {
background: white;
border-radius: 0.8rem;
color: rgb(50,50,50);
padding: 2rem;
}
/* Trumps */
<!doctype>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>koans</title>
<link rel="stylesheet" href="https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css">
<link rel="stylesheet" href="css/screen.css">
</head>
<body>
<div id="app"></div>
<script src="js/compiled/app.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.4.0/languages/clojure.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.4.0/languages/clojure-repl.min.js"></script>
<script>koans.core.init()</script>
</body>
</html>
When using Leiningen we have to create a project-manifest with all of the dependencies and procedures for building. Note that there’s a new kid on the block named `boot` that offers a more programmatic approach to this problem which often would be a better fit.
(defproject koans "0.0.1"
:description "Clojure(Script) for dummies"
:dependencies [[org.clojure/clojure "1.9.0-alpha14"]
[org.clojure/clojurescript "1.9.473"]
[org.clojure/test.check "0.9.0"]
[cljsjs/react-highlight "1.0.5-0"]
[nightlight "1.6.3"]
[re-frame "0.9.2"]
[re-frisk "0.3.2"]
[secretary "1.2.3"]
[ns-tracker "0.3.1"]
[reagent "0.6.0"]]
:plugins [[lein-cljsbuild "1.1.4"]]
:source-paths ["src/clj" "src/cljs"]
;; These paths will be removed by running `lein clean`
:clean-targets ^{:protect false} ["target"
"resources/public/css"
"resources/public/js/compiled"]
:figwheel {:css-dirs ["resources/public/css"]}
:repl-options {:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}
:profiles
{:dev
{:dependencies [[binaryage/devtools "0.9.1"]
[figwheel-sidecar "0.5.9"]
[com.cemerick/piggieback "0.2.1"]]
:plugins [[lein-figwheel "0.5.9"]
[lein-doo "0.1.7"]
;; `lein nightlight --port 4000 --url "http://localhost:3000` to start an editor on the port 4000
[nightlight/lein-nightlight "1.6.3"]]}}
:cljsbuild
{:builds
;; `lein figwheel dev` for pretty-printing, source-maps and code hot-loading
[{:id "dev"
:source-paths ["src/cljs"]
:figwheel {:on-jsload "koans.core/mount-root"}
:compiler {:main koans.core
:output-to "resources/public/js/compiled/app.js"
:output-dir "resources/public/js/compiled/out"
:asset-path "js/compiled/out"
:source-map-timestamp true
:preloads [devtools.preload]
:external-config {:devtools/config {:features-to-install :all}}}}
;; `lein cljsbuild min` for a production-build with dead-code removal and minification
{:id "min"
:source-paths ["src/cljs"]
:compiler {:main koans.core
:output-to "resources/public/js/compiled/app.js"
:optimizations :advanced
:closure-defines {goog.DEBUG false}
:pretty-print false}}
;; `lein test once` will build without optimizations and run all the tests specified in the runner
{:id "test"
:source-paths ["src/cljs" "test/cljs"]
:compiler {:main koans.runner
:output-to "resources/public/js/compiled/test.js"
:output-dir "resources/public/js/compiled/test/out"
:optimizations :none}}]})