Clojure 101 cheatsheet

After being tempted for a long time to learn a Functional Programming Language, I finally started dabbling with one this week. The small push I still needed was found when reading the book “The Unicorn Project” where one of the main characters in the book uses Clojure. The author of the book, Gene Kim, is a big fan of Clojure as well.

I’ve written some notes whilst learning Clojure this week using the book “Web Development with Clojure”. It’s written in Dunglish (Dutch and English mixed), however, since I wrote it for myself.

Een groot deel van een functional language is dat je met immutable variabelen zit. In een gewone taal kan je vars doorgeven "by value" of "by reference". Bij "by reference" weet je helemaal niet wat je gaat terugkrijgen. "by value" zit je met een performance hit omdat je altijd moet kopiĆ«ren… Als je variablen standaard niet wijzigbaar maakt dan moet je geen kopie doorsturen

Pure functies: functies die geen side-effects hebben. Je geeft ze data via argumenten en er komt altijd hetzelfde uit. Zo’n functie doet geen funky state business (==mag geen shared state updaten!) maar staat geisoleerd en op zijn eigen en is dus makkelijk om over te redeneren.

Declaratieve functies: Je scheidt de data van je logica door functies te maken die als argument de code bevatten om bepaalde data te handlen (ofzoiets?)

  • Null value: nil
  • Booleans: true, false
  • Numbers: 1, 1.3, 1/3, …
  • Keywords: beginnen met een ":", bvb :foo
  • Strings: met dubbele quotes (altijd) zoals "foo bar haha"
  • Characters: met een backslash ervoor: \a \b
  • Regexes: een string met een hekje ervoor: #"(ballekes)"
  • List: (1 2 3)
  • Vector: [1 2 3]
  • Map: {:foo "a" :bar "b"}
  • Set: #{"a" "b" "c"}

Functies: (defn square [x] (* x x)). Dit is eigenlijk de korte versie van een algemenere "def" waarmee je variabelen maakt. Je kan bijvoorbeeld een variable "square" maken die als waarde een anonieme functie krijgt (zie hier beneden): (def square (fn ([x] (* x x))))

  • Variabel aantal argumenten: (defn jaja [arg1 arg2 & argzzz] (...))

Functies callen: (functienaam arg1 arg2). Je kan enkel een functie callen die al gedeclareert is geweest (Clojure doet single-pass compiling), dus als je functie implementatie lager in de file zet dan degene die het aanroept, zet dan bovenaan de file (declare functieDieLagerStaat)

Functies kunnen ook anoniem zijn (zonder naam) zodat ze kunnen doorgegeven worden als argument aan andere functies :-). Bijvoorbeeld: ((fn [hihi] (println hihi)) "HALLO") print "HALLO". Via het hekje kan je ook zeggen dat iets een anonieme functie is. (fn [hihi] (println hihi)) wordt dan simpelweg #(println %). Als je meerdere argumenten hebt, gebruik dan %1, %2, …

Higher-order functies: Functies die functies als parameters ontvangen zijn zogezegd van hogere orde. Bijvoorbeeld map waar je een anonieme functie meegeeft die dan over de data wordt gehaald: (map #(+ % 1) [1 2 3 4 5])

  • Filter: (filter even? [1 2 3 4 5])
  • Remove: (remove empty? someList)

Closures: Functions that return functions.

Herschrijven van nested calls met "->>": Stel dat je iets nested hebt zoals: (reduce + (interpose 5 (map inc (range 10)))) dan kan je dat makkelijker schrijven als: (->> (range 10)(map inc)(interpose 5)(reduce +)). Als je dan iets wil herschikken is dat op deze manier van noteren doodeenvoudig.

(->> (range 1 6)
(map #(* % %))
(filter #(= (mod % 2) 0))
(run! println))

Ik snap nog niet helemaal wat dit wil zeggen maar blijkbaar kopieert Clojure niet altijd de temporary variabele (wnt immutable remember) want dat zou te traag zijn en dus doet het: "Clojure is backed by persistent data structures that create in-memory revisions of the data. Each time a change is made, a new revision is created proportional to the size of the change. With this approach we only pay the price of the difference between the old and the new data structures while ensuring that any changes are inherently localized."

Destructuring function arguments: Zoals je weet kan een functie argumenten ontvangen die het kan gebruiken. De notatie is hiervoor (defn ftie [arg1 arg2 & argzzz] ()). Met destructuring wordt eigenlijk bedoelt dat je niet gewoon een lijst als functie-argumenten hebt maar iets speciaals doet binnen die vierkante haakjes zoals:

  • (let [[small big] (split-with #(< % 5) (range 10))] (println small big)): een dynamische argument generator…
  • (defn print-user [[name address phone]] (println name address phone)): hier geef je dus eigenlijk een set mee aan de ftie dus 1 argument maar, maar in je ftie definitie zeg je dat er in dat eerste argument 3 waardes zullen steken en die geef je een naam die je meteen in je ftie body kan gebruiken…
    • Je breekt dus eigenlijk de argumenten op == destructuring
  • (let [{foo :foo bar :bar} {:foo "foo" :bar "bar"}] (println foo bar))
    • Korter kan ook: (let [{:keys [foo bar]}])

Namespaces: (ns ditIsEenNamespace) Net zoals in Java kan je in Clojure jouw code opdelen in namespaces. Als je iets wil gebruiken van een andere namespace kan je ":use" of ":require" gebruiken;

  • Use gebruiken: (ns ditIsEenNamespace (:use andereNamespace)), hiermee laadt je alle symbolen (variables) van de "andereNamespace" in jouw namespace voor gebruik.
  • Require: (ns ditIsEenNamespace (:require andereNamespace)), dan kan je wat verder in je code vars accessen met andereNamespace/varNaam, zoals je bij Java de hele klassepad zou kunnen typen…

Dynamic variables: Voor service connections, db connections, file streams, … heb je wel de kans om een variable een waarde te assignen. Maar gebruik dit enkel in zeer specifieke gevallen als je niet anders kan.

Hier hebben we een dynamische variabele die via een "binding" later de waarde "Haha" krijgt…

(declare ^:dynamic *hihi*)
(binding [*hihi* "Haha" (println *hihi*))

Polymorfisme kan via 2 manieren, door "defmulti" & "defmethod" te gebruiken of door "extend-protocol".

  • Defmulti laat je toe te specifieren welke functienaam je meerdere keren gaat definieren en aan de hand van welk keyword Clojure de juiste kan pakken.
  • Protocols zijn zoals interfaces in Java. Je definieert een lijst van functies die moeten geĆÆmplementeerd worden.

Voorbeeld defmulti en defmethod:

(defmulti toeter :merk)
(defmethod toeter :citroen [& args] (println (str "Kleine toet")))
(defmethod toeter :mercedes [& args] (println (str "Grote toet")))
(defmethod toeter :default [& args] (println (str "Toet")))
(toeter {:merk :mercedes})

Merk op dat in "defmulti" je niet enkel een keyword kan gebruiken maar ook arbitraire functties zoals: (defmulti ftieNaam (fn [x y] [(:role x) (:role y)]))

Voorbeld defprotocol:

(defprotocol Auto
  "Auto protocol documentatie"
  (toeter [this] "druk op de toeter")
  (rij [this] [this rijstijl] "laat de auto rijden")
)

(deftype Mercedes [type] Auto
  (toeter [this] (println (str "Grote toet")))
  (rij [this] (println (str "vroem")))
  (rij [this rijstijl] (if (= rijstijl (str "power")) (println (str "powaaah")) (println (str "vroem"))))
)

Global state & concurrent programming: via Software Transactional Memory system (STM) wordt een update aan globale state altijd atomisch uitgevoerd zonder dat je hiervoor locks of mutexes ofzo moet gebruiken.

  • Atom: indien je geen coordinatie ofzo nodig hebt maar gwn een var wil updaten
  • Ref: voor transacties mbv "dosync"

Atom voorbeeld met shorthand ‘@’ en fties met een uitroepteken als conventie voor fties die op mutable data opereren:

(def globalVar (atom nil))
(println (deref globalVar))
(ptintln @globalVar)
(reset! globalVar 1337)
(swap! globalVar inc)
(println @globalVar)
=> 1338

Ref voorbeeld:

(def names (ref [])) // "names" is een lege array
(dosync // Start een transactie
	(ref-set names ["Adriaan"])
	(alter names #(if (not-empty %) (conj % "Jane") %))
)

Zie ook https://clojure.org/about/concurrent_programming

Macros: Zoals in C worden macro’s uitgevoerd at compile time en werk je dan in je code met een ftie waar die macro’s worden ingezet. Het is een beetje een ingewikkelde syntax en niet zo makkelijk dus ik ga gen moeite doen om dat hier al neer te pennen.

Calling Java:

  • Importeren van een Java class: (ns mijnNamespace (:import java.io.File))
    • Of meerdere: (ns jaja (:import [Java.io.File FilInputStream]))
  • Instantieren: (new File "mijnfile.txt") of korter met een puntje: (File. "mijnfile.txt")
  • Methodes uitvoeren: (let [f (File. "mijnfile.txt")] (println (.getAbsolutePath f)))
    • Dus met een puntje ervoor als onderscheid met normale Clojure fties
    • Static methods met ‘/’: (str File/separator "foo")

Chaining van Java methodes en shorthand:

(.getBytes (.getAbsolutePath (File. "mijnfile.txt")))
// Of korter met 2 puntjes:
(.. (File. "mijnfile.txt") getAbsolutePath getBytes)

Reader conditionals: Je kan je Clojure compilen naar Java maar ook naar Javascript. Dit vereist dat je soms code moet schrijven die werkt indien naar Javascript of naar Java getranspiled.

(defn current-time []
  #?(:clj (.getTime (java.util.Date.))
     :cljs (.getTime (js/Date.))
  )
)

// Of met #?@ als je meerdere elementen wil teruggeven
(:require
 [clojure.string :as string]
 #?@(:clj  [[clojure.pprint :refer [pprint]]
            [clojure.java.io :as io]]
     :cljs [[cljs.pprint :refer [pprint]]
            [cljs.reader :as reader]]))

// Wordt voor Java bvb:
(:require
 [clojure.string :as string]
 [clojure.pprint :refer [pprint]]
 [clojure.java.io :as io])

Leave a comment

Your email address will not be published.