Lab · Playground
Clojure playground
Clojure in your browser through Scittle — immutable data, lazy sequences, transducers, threading macros, atoms, and multimethods.
Rich Hickey released Clojure in 2007: a Lisp for the JVM built around one idea, that most bugs come from mutable state shared between parts of a program. Its maps, vectors and sets are immutable but cheap to "change" — a new version shares almost all of its structure with the old one — and its standard library is a big vocabulary of functions over sequences. It is code as data, too: every program below is itself a list of lists, vectors and maps.
These blocks run Scittle, which puts the Small Clojure
Interpreter in the browser. It is about 1 MB, so it loads quickly. It follows ClojureScript, so
numbers are JavaScript doubles (there is no BigInt arithmetic) and Java interop is JavaScript
interop. println output appears as usual, and the value of the last form is shown after ⇒.
Each run starts afresh.
(def order {:id 1042
:customer {:name "Ada" :city "London"}
:items [{:sku "pen" :qty 3 :price 1.5}
{:sku "ink" :qty 1 :price 7.25}]})
(def shipped (-> order
(assoc :status :shipped)
(update-in [:customer :city] clojure.string/upper-case)
(update :items conj {:sku "nib" :qty 10 :price 0.4})))
(println "original:" (:status order) (get-in order [:customer :city]) (count (:items order)))
(println "shipped: " (:status shipped) (get-in shipped [:customer :city]) (count (:items shipped)))
(println "first item shared, not copied?" (identical? (first (:items order))
(first (:items shipped))))
(reduce + (map #(* (:qty %) (:price %)) (:items shipped)))
;; Infinite sequences are fine: nothing is computed until something asks
(def fibs (map first (iterate (fn [[a b]] [b (+ a b)]) [0 1])))
(println (take 15 fibs))
(println (take 5 (filter even? fibs)))
;; The same functions work on vectors, maps, sets, strings…
(println (frequencies "mississippi"))
(println (group-by count ["ox" "cat" "dog" "bird" "ewe" "frog"]))
(println (partition 3 1 [1 2 3 4 5 6]))
(println (interleave [:a :b :c] (range)))
;; Sum of the squares of the odd numbers below 100, read from top to bottom with ->>
(->> (range 100)
(filter odd?)
(map #(* % %))
(reduce +))
;; A transducer is a recipe for transforming a step function — independent of where data comes from
(def clean-words
(comp (map clojure.string/lower-case)
(map #(clojure.string/replace % #"[^a-z]" ""))
(remove clojure.string/blank?)
(remove #{"the" "a" "of" "and"})
(distinct)))
(def text "The Owl and the Pussy-cat went to sea, in a beautiful pea-green boat; the Owl looked up.")
(def words (clojure.string/split text #"\s+"))
(println (into [] clean-words words)) ; collect into a vector
(println (transduce clean-words (completing (fn [n _] (inc n))) 0 words) "distinct words")
(println (transduce (comp clean-words (map count)) max 0 words) "letters in the longest")
(println (sequence (comp clean-words (take 3)) words)) ; stops early: take is a transducer too
(into #{} (comp clean-words (filter #(> (count %) 4))) words)
(defn describe [{:keys [name langs] :or {langs []} :as person}]
(let [[first-lang & others] langs]
(str name " writes " (or first-lang "nothing yet")
(when (seq others) (str ", also " (clojure.string/join " and " others)))
(when (:retired person) " (retired)"))))
(println (describe {:name "Grace" :langs ["COBOL" "FLOW-MATIC"] :retired true}))
(println (describe {:name "Rich" :langs ["Clojure" "Lisp" "Java"]}))
(println (describe {:name "Newbie"}))
;; Multi-arity, variadic, and higher-order
(defn mean
([xs] (/ (reduce + xs) (count xs)))
([x & more] (mean (cons x more))))
(println (mean [1 2 3 4]) (mean 10 20))
(def shout (comp clojure.string/upper-case #(str % "!")))
(map shout ["hey" "you"])
;; The one controlled place to put changing state: swap! applies a pure function to it
(def inventory (atom {:widgets 10 :gadgets 3}))
(defn sell! [item n]
(swap! inventory
(fn [inv]
(if (>= (get inv item 0) n)
(update inv item - n)
(do (println " can't sell" n (name item)) inv)))))
(add-watch inventory :log (fn [_ _ old new]
(when (not= old new) (println " changed:" old "->" new))))
(sell! :widgets 4)
(sell! :gadgets 5)
(swap! inventory assoc :gizmos 7)
@inventory
;; A multimethod dispatches on any function of its arguments — here, a map's :shape
(defmulti area :shape)
(defmethod area :circle [{:keys [r]}] (* Math/PI r r))
(defmethod area :rect [{:keys [w h]}] (* w h))
(defmethod area :default [s] (throw (ex-info "unknown shape" s)))
(println (map #(-> % area Math/round)
[{:shape :circle :r 2} {:shape :rect :w 3 :h 4}]))
(println (try (area {:shape :blob}) (catch :default e (str "error: " (ex-message e)))))
;; A protocol is a set of functions that types implement; records are maps with a type
(defprotocol Priced (price [x]) (describe [x]))
(defrecord Book [title cost] Priced
(price [_] cost)
(describe [b] (str "\"" title "\" " (price b))))
(defrecord Bundle [items discount] Priced
(price [_] (* (- 1 discount) (reduce + (map price items))))
(describe [b] (str (count items) " items for " (price b))))
(def bundle (->Bundle [(->Book "SICP" 40) (->Book "HtDP" 35)] 0.2))
(println (describe bundle))
(assoc (->Book "SICP" 40) :cost 30)