Quick reference to the basics of Clojure
◈ Website ◈
The listing sheet, as PDF, can be found here, while below is an unruly html rendition.
This reference sheet is built around an Org-mode CheatSheet system.
- Primitive Data Structures
- Sequence & Collection Methods
- Laziness: “I thunk, therefore I'm done!”
- Conditionals
- Block of Code
do
and control flowand, or
- Functions
- Let and Loop
- Quotes, Quasi-Quotes, and Unquotes
- Macros
Everything is a list!
-
Functions are first-class values; variables and functions share the same namespace —“Clojure is a Lisp-1 Language”.
-
To find out more about
name
execute(doc name)
!
For these notes, I followed the delightful Clojure for the Brave and True; I suggest starting with ClojureScript Koans: An interactive question-answer approach where you type up (super small) code to progress ;-)
Use def
to bind a name to a value —this' like CommonLisp's setq
.
Clojure's data structures are all immutable; that cannot be changed in place.
-
Numbers: Integers, floats, ratios:
(printf "%s and %s and %s" -93 1.2 1/5)
-
Symbols: Atomic literals,
'hello
. They're like numbers: They are what they are! Unlike strings, the idea is to treat these literally rather than operate on their ‘contents’. They're first-class names!Closely related, are keywords, such as
:hello
. In Clojure, these are not symbols since they always evaluate to themselves and cannot act as names.It's a semantic difference: With
eval
, the symbol'hello
will have whatever value the namehello
has; whereas:hello
yields:hello
as its value. -
Strings: Enclosed in double-quotes; use
str
to catenate a bunch of lists. -
Maps/Hashes/Dictionaries/property-lists: These structures allow you to associate keys/words/properties with values/definitions. Extremely versatile!
-
The empty map is
{}
; -
The map sending
keyᵢ
tovalueᵢ
is{:key₀ value₀ ... :keyₙ valueₙ}
.(def m {:name {:first "Bob" :last "Abe"} :age 10 :act +}) (m :age) ;; ⇒ 10 (:last (:name m)) ;; ⇒ "Abe" (apply (get m :act) [1 2]) ;; ⇒ 3 ;; Keys can be anything ({3 40} 3) ;; ⇒ 40
-
If
m
is a map, use(:key m)
to get the value associated with:key
. -
Since maps are essentially functions, you can use function application syntax:
(m key)
. -
Trying to get the value associated to a key not in a map will yield
nil
.Three equivalent ways to get the value of
k
in mapm
if it is inm
, otherwise return default valued
—which is optional and defaults tonil
.(get m :k d)
,(m :k d)
,(:k m d)
In particular, observe that keywords
:k
act as functions that lookup a key in a map —symbols also act as look-up functions. -
Starting from the empty map, one can ‘conjoin’ new values:
(conj {k v} m)
is the new map that at keyk
yieldsv
and at all other keys behaves likem
. -
Alternatively, use
(hash-map :key₀ value₀ ... :keyₙ valueₙ)
to make a map and(get m key)
to get the value associated with keykey
.
-
-
Vectors: (Append friendly!) These are like maps but numbers are used to access the values. E.g.,
(get [x₀ x₁ ... xₙ] i) = xᵢ
fori ∶ 0..n
.- Vectors can also be created with
(vector x₀ x₁ ... xₙ)
. - Use
conj
to ‘conjoin’ a new element to the end of a vector:(conj xₙ₊₁ [x₀ ... xₙ]) = [x₀ ... xₙ xₙ₊₁]
.
- Vectors can also be created with
-
Lists: (Prepend friendly!) These are like vectors but
conj
adds elements to the start of a list.get
doesn't work with lists; use(nth '(x₀ ... xₙ) i) = xᵢ
.- Lists can also be created with
(list x₀ x₁ ... xₙ)
.
For lists,
conj
is also known ascons
since it constructs lists by adding elements at the start. The operation to add elements at the end is known as snoc, the reverse reading of cons; as such, Clojure has both (cons) lists and snoc lists (vectors) as primitive data structures. -
Sets: Collections of unique values,
#{x₀ ... xₙ}
—duplication is ignored in that(conj xs x)
is justxs
ifx
is already a member of setx
.- Sets can also be created with
(hash-set x₀ x₁ ... xₙ)
. (contains? xs x)
indicates whetherx
is an element of setxs
.- Sets are essentially maps where the values are just themselves:
#{x₀ ... xₙ} = {x₀ x₀ ... xₙ xₙ}
; so you can use any of the three above ways toget
values.
- Sets can also be created with
Unlike many other Lisps, in Clojure maps, vectors, lists, and sets can
(optionally) have their arguments separated by commas!
E.g.,
(= '(1 2 3) '(1, 2, 3) '(1 2, 3))
is true.
All of Clojure's data structures —strings, vectors, maps, lists, and sets— take part in both the sequence abstraction and the collection abstraction.
The sequence abstraction focuses on the elements, whereas the collection abstraction focuses on the structure as a whole.
The Collection Abstraction
-
count
⇒ How many elements does the collection have? -
empty?
⇒ Does it have any elements? -
every?
⇒ Does every element in the collection satisfy a given predicate? -
into
⇒ Insert all the elements from the second collection into the first.- E.g.,
(into #{} xs)
convertsxs
into a set, whereas(into {:a 1} xs)
converts a listxs
of key-value vector pairs into a map but:a
will map to whatever it maps to inxs
, if any, or 1 otherwise.
This is like
concat
but it does a collection conversion: The result is the same collection type as the first argument. - E.g.,
-
(conj xs x)
⇒ Insertx
into the collectionxs
.- For vectors, insert at the end.
- For lists, insert at the beginning.
- For sets, insert if it's not already in
xs
. - For maps,
x
has the shape{:key value}
, and we update the value of:key
inxs
to now be associated withvalue
.
This is like
cons
but it preserves the collection type —whereascons
forces it to be a list:(cons x xs) = (cons x (seq xs))
where(seq xs) = (into '() xs)
is discussed below.(conj xs x) = (into {} (cons x xs))
forxs
a map.conj
andinto
are essentially the same function, except one takes a varadic number of arguments whereas the other takes a formal sequence for the second argument:(conj xs y₀ y₁ ... yₙ) = (into xs (list y₀ y₁ ... yₙ))
In summary, every collection can be formed from the empty collection —
'(), [], {}, #{}
— and using into
to shove new elements into it —alternatively, conjoining new
elements with conj
. Interestingly,
(into '() xs)
reverses a list xs
but does nothing to
a vector xs
—since conj
prepends for lists and appends for vectors.
The Sequence Abstraction
Sequences implement first, rest, cons
and so may use
the sequence operations map, reduce, filter, distinct, group-by, ...
.
Using first
and rest
we
can always obtain a list from any sequence type; the method to do so is called
seq
and it's characterised by: (seq xs) = (cons (first xs) (seq (rest xs)))
. For instance, for maps,
(seq {key₀ val₀ ... keyₙ valₙ}) = '([key₀ val₀] ... [keyₙ valₙ])
. Moreover, whenever a sequence is expected, seq
is called —e.g., when
map
is called. This is why map
always
returns a list; e.g., (map #(* 3 (second %)) {:a 1 :b 3 :c 5}) = '(3 9 15)
. Use into
—discussed
above— to convert to a different sequence type.
Below are a few examples shown using lists, but they work with the other data-structures too, such as hashmaps!
-
(range start end) = (list start (+ start 1) (+ start 2) ... (dec end))
.start
may be omitted, defaulting to 0. -
(concat '(x₀ ... xₖ) '(xₖ xₖ₊₁ ... xₙ)) = '(x₀ ... xₙ)
. -
(some p xs) = (p xₖ)
wherexₖ
is the first element to satisfy predicatep
; or nil otherwise. (Linear Search)(some #(and (p %) %) xs) = xₖ
is how to get the actual element that satisfies the predicatep
, if any. -
map
zips its lists arguments together along a given “zipping function”f
.(nth (map f xs₀ xs₁ ... xsₙ) i)
=(f (nth xs₀ i) (nth xs₁ i) ... (nth xsₙ i))
For
f
beingvector
, themap
takes some lists (“rows”) and produces a list of lists (“columns”) —think “matrix transpose” or “tupling”.Recall that
:key
act as functions extracting the values associated with key:key
; whence,(map :key ms)
projects the value of:key
from each map inms
. -
reduce
replaces every (implicit)cons/conj
with a new binary operation.(reduce ⊕ e '(x₀ x₁ ... xₙ)) = (⊕ (⊕ (⊕ (⊕ e x₀) x₁) ...) xₙ)
The initial value
e
is optional and may be omitted. -
take k '(x₀ x₁ ... xₙ)) = '(x₀ x₁ ... xₖ₋₁)
-
drop k '(x₀ x₁ ... xₙ)) = '(xₖ xₖ₊₁ ... xₙ)
-
take-while p '(x₀ x₁ ... xₙ)) = '(x₀ x₁ ... xₖ₋₁)
wherexₖ
is the first element to not satisfy the predicatep
. -
drop-while p '(x₀ x₁ ... xₙ)) = '(xₖ xₖ₊₁ ... xₙ)
wherexₖ
is the first element to not satisfy the predicatep
. -
Extensionality:
xs = (concat (take-while p xs) (drop-while p xs))
-
(filter p xs)
is the largest subsequence ofxs
whose elements all satisfy predicatep
; unliketake-while
, it process all ofxs
rather than stopping at the first value that falsifies the predicatep
. -
sort
sorts a list in ascending order. -
(sort-by f xs)
sorts the elements ofxs
according to the order ‘⊏’ defined byx ⊏ y ≡ f x < f y
. E.g.,(sort-by count xss)
sorts the sequence of sequencesxss
according to their length.
There is also list comprehensions
(for [x xs] body) = (map (fn [x] body) xs)
;
more generally, these are “nested for-loops”:
(for [x₀ xs₀ ... xₙ xsₙ :let 𝓁𝒸 :when 𝓌𝒸] body)
= (map (fn [[x₀ x₁ ... xₙ]] (let 𝓁𝒸 body))
(filter (fn [[x₀ x₁ ... xₙ]] (let 𝓁𝒸 (when 𝓌𝒸 [x₀ ... xₙ])))
(cartesian-product xs₀ xs₁ ... xsₙ)))
Where a supposed cartesian-product
function
essentially behaves as (for [x₀ xs₀ ... xₙ xsₙ] [x₀ ... xₙ])
—i.e., it returns all vectors vs
where
(nth vs i)
is an element of xsᵢ
.
The map
(and filter
) function is lazy: The iᵗʰ-element of (map f xs)
is computed
only when it is actually needed. E.g., (first (map f xs))
is (f (first xs))
and
so the rest of the map is not evaluated at all.
Likewise, (def result (map f xs))
is evaluated nearly instantaneously regardless
of how big xs
may be: The map
is computed as elements of result
are accessed.
If you try to access or “think” of (nth result i)
then if it is already computed
—i.e., we have already “thunk” it— then we return that value, otherwise, we
compute it and return it.
More accurately, Clojure chunks its computations: When an element is requested, it will compute a few elements after it as well since you're likely to request them as well. From below, it can be seen that it computes the next 30 elements.
(def result (map #(do (Thread/sleep 100) (* 2 %)) (range 0 100)))
(time (nth result 1)) ;; ⇒ 2; Elapsed time: 3270.204959 msecs
(time (nth result 30)) ;; ⇒ 60; Elapsed time: 0.051002 msecs (Neato!)
(time (nth result 33)) ;; ⇒ 66; Elapsed time: 3293.824322 msecs
Accessing the first element takes ~3200 milliseconds since Clojure prepared the next ~30 elements in case we want to access them next; e.g., in the next line we access the 30th element almost instantaneously. After that, we try to access an element not yet computed and it and the next ~30 after it are computed.
Note:
-
(Thread/sleep 1000)
⇒ Sleep/pause for 1second -
(time e)
⇒ Evaluatee
and return its value along with a print to standard output indicating how long it took to evaluatee
.(+ 2 (time (+ 1 2))) ;; ⇒ 5 (Elapsed time: 0.022374 msecs)
Warning: Unused = Unevaluated!
;; Since the map's result in unused, it is not evaluated!
(do (map print (range 0 100)) (println "bye")) ;; Prints' “bye” only
;; Use “mapv” which is eager!
(do (mapv print (range 0 100)) (println "bye")) ;; Prints' “01...100bye” only
Infinite sequences:
(repeat x)
is the infinite sequence that returns x
at every index:
(nth (repeat x) i) = x
for any i ≥ 0.
Evaluating (repeat x)
will take forever since it's
an infinite list; instead use (take n (repeat x))
to
get a finite list of length n
.
Likewise, (repeatedly f)
generates an infinite
sequence from the nullary function f
.
(take 3 (repeatedly (fn [] (rand-int 10)))) ;; ⇒ '(5 9 6)
(take 2 (repeat "n")) ;; ⇒ '("n" "n")
You can also use lazy-seq
to treat a sequence lazily.
(defn evens
([] (evens 0))
([n] (cons (* 2 n) (lazy-seq (evens (inc n))))))
(take 3 (evens)) ;; ⇒ '(0 2 4)
You can memoize a function f
so that if you've
already ‘thunk’ it at input a
then anytime
(f a)
you get the value immediately!
(defn slow [x] (Thread/sleep 1000) x)
(def fast (memoize slow))
(time (fast 1)) ;; ⇒ “Think it!” Elapsed time: 1000.529943 msecs
(time (fast 1)) ;; ⇒ “Already thunk it!” Elapsed time: 0.068761 msecs
Booleans: true
and false
- (Deep structural) equality:
(= x y)
. nil
indicates no value.- (Warning! Unlike other Lisps,
nil ≠ false
andnil ≠ '()
! )
- (Warning! Unlike other Lisps,
- Use
nil?
to check if a value isnil
. - Comparisons: As expected; e.g.,
(<= x y)
denotes x ≤ y.
Regarding Boolean operations —such as and, or, if
— both nil
and false
denote
logically false values; everything else denotes logical truth: (boolean x)
returns true exactly when x
is truthy —i.e., it converts things to Booleans.
or, and
returns the first truthy / fasley value if any, or the last value otherwise.if
takes at-most 3 arguments:(if condition thenExpr optionalElseExpr)
- If you want to perform multiple expressions, use
do
—this isprogn
in CommonLisp. - Note:
(if x y) = (if x y nil)
; better:(when c thenBlock) = (if c (do thenBlock))
. (if xs ...)
means “if xs is non-fasley then …” is akin to C style idioms on linked lists. E.g.,(if 9 2 4) = 2
.
- If you want to perform multiple expressions, use
Avoid nested if-then-else clauses by using a cond
statement –a (lazy) generalisation of switch statements: It sequentially
evaluates the expressions testᵢ
and performs only
the action of the first true test; yielding nil
when
no tests are true. Below we use the keyword :else
to
simulate a ‘default case’—indeed, any non-falsey value would have sufficed.
(cond
test₀ expr₀
test₁ expr₁
⋮
:else defaultExpr)) ;; optional
Replacing cond
by case x
results in an exhaustive case-analysis: If x
is the
literal expression testᵢ
then yield
exprᵢ
, if no match happens, crash —to use a
default, just have a default value as the final expression, no need to precede
it by anything. A hybrid of cond
and
case
is condp
.
Use the do
function to treat multiple expressions as a single expression. E.g.,
(do (println "hello")
(def x (if (< 2 3) 'two-less-than-3))
(println (format "%s" x))
23) ;; Return value of the “do” block
This’ like curly-braces in C or Java. The difference is that the last expression is considered the ‘return value’ of the block.
-
Lazy conjunction and disjunction ---
and, or
— can be thought of as control flow first and Boolean operations second:(and s₀ ... sₙ e) ⇒ when all xᵢ are non-falsey, do e (or s₀ ... sₙ e) ⇒ when no xᵢ is falsey, do e
-
That is,
and
is the maybe monad: Perform multiple statements but stop when any of them fails, returns falsey. -
Likewise,
or
performs multiple statements until one of them succeeds, returns non-falsey: -
We can coerce a statement
sᵢ
to returning non-falsey as so: (do sᵢ true)
. Likewise, coerce falsey by(do sᵢ nil)
.
Functions are (unexceptional) data:
;; Add numbers
(+ 1 2 3) ;; ⇒ 6
;; Apply a function to each number
(map inc [2 4 6]) ;; ⇒ (3 5 7)
Function invocation: (f x₀ x₁ ... xₙ)
. E.g., (+ 3 4)
or (print "hello")
.
-
Clojure is strict: In a function call, all arguments are evaluated before the function is executed. Besides function calls, there are special forms which are ‘special’ since they don't always evaluate all of their operands; e.g.,
if
,when
, and macros such asdef
. There are also ‘lazy collection’ types. -
Only prefix invocations means we can use
-,+,*
in names since(f+*- a b)
is parsed as applying functionf+*-
to argumentsa
andb
. -
Function definition:
(defn my-function "This functions performs task ..." ;; documentation, optional [arg₀ arg₁ & more] ;; header, signature (str arg₀ arg₁ (nth more 3))) ;; body, instructions
-
The return value of the function is the result of the last expression executed.
-
The documentation string may indicate the return type, among other things.
(doc my-function) ;; ⇒ See signature and docstring
-
In function definition, use
&
to make a function take any extra number of arguments; it must come at the end and it is treated as a list in the function body. This is known as a rest parameter.(my-function "x" "y" "a" "b" "c" "d") ;; ⇒ "xyd"
-
Functions also support arity overloading, which act as a way to support default arguments.
(defn doit
([x y z]
(+ x y z))
([x y]
;; default value for z
(doit x y 666))
([x]
;; Completely different behaviour
;; when y,z omitted
(* 7 x)))
(doit 1 2 3) ;; ⇒ 6
(doit 1 2) ;; ⇒ 669
(doit 1) ;; ⇒ 7
Instead of dispatching/choosing function definitional clauses according to
arity, some language also allow dispatch according to argument type; Clojure
goes further by dispatching along the result of an arbitrary function of the
arguments. These functions are called multimethods; they are defined by
defmulti
which takes their name and the dispatch
function, then each overloaded method is defined independently using
defmethod
which takes the multimethod name, the
dispatch value, and the function body.
;; “doit” is an overloaded method, dispatched along
;; the “:kind” value of the 1ˢᵗ arg, being a hashmap,
;; AND along the length of the second argument, being a sequence.
;; That is, the dispatch value is a vector of length 2.
(defmulti doit (fn [x y] [(:kind x) (count y)]))
;; Here are three boring implementations, along the dispatch value
(defmethod doit [:fruit 5] [x y] :one)
(defmethod doit [:fruit 4] [x y] :two)
(defmethod doit [:veggie 4] [x y] :three)
;; Optional default method when nothing matches
(defmethod doit :default [x y] x)
;; Example calls
(doit {:kind :fruit :age 10} "hola!") ;; ⇒ :one
(doit {:kind :veggie :age 10} "hola!") ;; ⇒ the 1ˢᵗ arg
;; ⇒ Without the default method, this crashes!
Multimethods are another way to define ordinary functions, and so can be used
wherever a function is used. Such functions could be defined using
condp
to dispatch along the appropriate
implementation; but this is more difficult to maintain as more alternatives are
added, whereas the defmethod
approach is a nice
separation of concerns —also known as open-closed principle.
You can also destructure parameters —lists and maps are discussed below.
(defn doit2
[ [x y &zs] ;; The first argument is a list/vector consisting
;; of at least 2 values, say x and y, then
;; followed by the list/vector zs
{w :w} ;; The second argument is a map;
] ;; Let w denote the value of key :w in the map.
(+ y (or w 1))) ;; In-case :w is not in the map, use 1 as default.
(doit2 [1 2 3 4] {:v 5 :w 6}) ;; ⇒ 7
;; (doit2 [1] {:v 5 :w 6}) ;; ⇒ Error: First argument too short!
Anonymous functions: (fn [arg₀ ... argₖ] bodyHere)
; as usual, you can also
destructure arguments and have rest parameters.
;; make then later invoke
(def my-f (fn [x y] (+ x y)))
(my-f 1 2) ;; ⇒ 3
;; make and immediately invoke
((fn [x y] (+ x y)) 1 2) ;; ⇒ 3
;; Super terse notation
(map #(* % 2) [1 2 3]) ;; ⇒ [2 4 6]
The anonymous function #(...%i...)
is a function where %i
refers to the i-th
argument; %
is equivalent to %1
and %&
refers to the rest parameter.
Keyword/named arguments = rest parameters + (hash)maps + destructuring.
(defn f [a & {:keys [b c d] :or {c 7 d 11}}] (list a b c d))
Huh? f
has a required argument, a
, followed by a rest parameter (&
), that we
destructure into a hashmap {...}
, whose :keys
contain at least b, c, d
---:or
, if
the key c
is not there then use 7 as default, and likewise use 11 as default for
d
.
;; (f) ⇒ Error, “a” is a required positional argument!
(f 2) ;; ⇒ '(2 nil 7 11)
(f 2 :d 4) ;; ⇒ '(2 nil 7 4)
(f 2 :c 5 :b 3 :d 7) ;; ⇒ '(2 3 5 7)
(f 2 :ignored 99 :whatever "ok") ;; ⇒ '(2 nil 7 11)
Higher order functions
-
apply
unpacks a sequence so it can be passed to a function that expects a rest parameter. For any k ≥ 0,(apply f x₀ x₁ ... xₖ₋₁ '(xₖ xₖ₊₁ ... xₙ)) = (f x₀ x₁ ... xₙ)
-
If
f
is a function ofn
arguments, then(partial f x₀ x₁ ... xₖ₋₁)
is the functionf
with it's firstk
arguments already given and the remainingn - k
elements not yet given.(partial f x₀ x₁ ... xₖ₋₁) = #(apply f x₀ x₁ ... xₖ₋₁ %&)
-
(complement p) = #(not (apply p %&))
-
(identity x) = x
-
(comp f₁ f₂ ... fₙ) = #(f₁ (f₂ (... (apply fₙ %&))))
; if n = 0, this is justidentity
.
Clojure's data structures are immutable —one cannot change them in-place—
with structural sharing and it has no assignment operator —one cannot
associate a new value with a name without creating a new scope, say via let
.
Let forms allow local names and allow destructuring as functions do.
(let [x 2
[_ y] '(0 1 2 3)] ;; y is 1
(+ x y)) ;; ⇒ 3
;; Let's add the first 10 numbers
(loop [i 0
sum 0]
(if (< i 10)
;; “update variables and continue”
(recur (inc i) (+ sum i))
;; “break, returning this value”
sum))
;; ⇒ 45
Loops are an in-place form of recursion: (loop [i start] ...(recur i′)...) = (f start)
where f
is (fn [i] ...(f i′)...)
. The loop, like the underlying anonymous
function, can take any number of arguments/initial-bindings.
Quotes: 'x
refers to the name rather than the value of x
.
- This is superficially similar to pointers:
Given
int *x = ...
,x
is the name (address) whereas*x
is the value. - The quote simply forbids evaluation; it means take it literally as you see it rather than looking up the definition and evaluating.
- Note:
'x = (quote x)
.
Akin to English, quoting a word refers to the word and not what it denotes.
This lets us treat code as data! E.g., '(+ 1 2)
evaluates to (+ 1 2)
, a function
call, not the value 3
! In general, (eval (quote e)) = e
for any expressions e
;
e.g., '(+ 1 2)
is data that when evaluated/executed/run yields 3: (eval '(+ 1 2)) ⇒ 3
.
An English sentence is a list of words; if we want to make a sentence where some
of the words are parameters, then we use a quasi-quote –it's like a quote, but
allows us to evaluate data if we prefix it with a tilde “ ~
”. It's usually the
case that the quasi-quoted sentence happens to be a function call! In which
case, we use eval
which executes code that is in data form; i.e., is quoted.
( Macros are essentially functions that return sentences, lists, which may happen to contain code. )
;; Quotes / sentences / data
'(I am a sentence)
'(+ 1 (+ 1 1))
;; Executing data as code ;; ⇒ 3
(eval '(+ 1 (+ 1 1)))
(def name "Jasim")
;; Quasi-quotes: Sentences with a
;; computation, code, in them.
`(Hello ~name and welcome)
;; ⇒ '(Hello "Jasim" and welcome)
`(+ 1 ~(+ 1 1)) ;; ⇒ '(+ 1 2)
As the final example shows, Lisp treats data and code interchangeably. A language that uses the same structure to store data and code is called ‘homoiconic’.
…when I have time…!