Skip to content

bdo-labs/koans

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Koans

Up and Running

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.

Some Terminology

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.

Getting down to Business

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

Source

Core

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))

Application State

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 {}})

Routes

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!))

Utils

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))

Views

(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])))

Koans

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 ;)")]]]))

Events

(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)))

Subscriptions

(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))))

Sprinkle some Styles

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 */

Boilerplate html

<!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>

Manifest

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}}]})

Releases

No releases published

Packages

No packages published