Skip to content

Instantly share code, notes, and snippets.

@theronic
Last active January 9, 2026 12:16
Show Gist options
  • Select an option

  • Save theronic/b6fa9ef78a0f96acf2b76e48ab0d2674 to your computer and use it in GitHub Desktop.

Select an option

Save theronic/b6fa9ef78a0f96acf2b76e48ab0d2674 to your computer and use it in GitHub Desktop.
Clojure Lesson 2026-01-06
; Introduction to Clojure
; =======================
; This note teaches you the entire syntax of Clojure in ~15 minutes.
; Clojure is a Lisp (short for LISt Processing)
; Lisp was invented in the 60s by John McCarthy and pionereed a number of ideas that are now
; commonplace in computer science, including:
; - tree data structures,
; - automatic memory management (garbage collection),
; - dynamic typing,
; - conditionals (if-statements),
; - higher-order functions (like map & apply),
; - recursion (when a function calls itself),
; - the self-hosting compiler, and
; - and the read–eval–print loop.
; Clojure is a modern dialect of Lisp that was designed as a hosted language, i.e. it sits on top of another platform
; like the JVM. The JVM has hundreds of man-years of optimization behind it and runs on every platform.
; There are implementations of the Clojure language for JVM, JavaScript (ClojureScript),
; the CLR (ClojureCLR) and Dart (ClojureDart). Same language, different platform.
; Clojure is good for anything you would use Java for, but much more dynamic and succinct.
; It is especially suited to applications that deal with information: ingest, transform and output. Information being something that is true at some moment in time.
; Calling Functions
; =================
; There is only one rule in Clojure and it's called The Operational Form:
(operator arg1 arg2 arg3 ...)
; ...where `operator` is one of a function, a macro, or a special form
; `arg1`, `arg2` & `arg3` are arguments or operands.
; E.g. to add up numbers, we call the plus function, which is bound to the `+` symbol:
(+ 1 2 3)
=> 6
; or to subtract numbers:
(- 10 6)
=> 4
; (we will discuss macros and special forms later)
; 99% of the time, we call functions.
; Calling the `range` function returns a list of 10 numbers, 0–9:
(range 10)
=> (0 1 2 3 4 5 6 7 8 9)
; The `first` function returns the first element from a collection (list, vector, map or set):
(first (range 10))
=> 0
; Data Structures
; ===============
; There are 4 core data structures:
; 1. lists
; 2. vectors
; 3. sets,
; 4. and maps.
; Lists:
(list 1 2 3)
=> (1 2 3)
; or you can simply write:
'(1 2 3)
=> (1 2 3)
; Note: the `'` quote character is short for `quote` and it avoids the list from being evaluated as operational form.
; Without the `'` quote first argument, the evaluator will try to call the number `1` as a function, e.g.
(1 2 3)
=> throws "class java.lang.Long cannot be cast to class clojure.lang.IFn"
; Lists are implemented as singly-linked lists.
; They grow from the front and are not not indexed, i.e. O(n) access time for big lists.
; Vectors (or arrays):
(vector 1 2 3)
=> [1 2 3]
; or you can simply write square brackets to get a vector:
[1 2 3]
=> [1 2 3]
; Vectors are like lists, but they grow from the back.
; Vectors are indexed, so they have O(1) access time (at additional memory cost).
; Sets are like vectors and indexed with unique values:
(set [1 2 3])
=> #{1 3 2}
; or use #{...}
#{1 2 3}
=> #{1 3 2}
; Sets don't allow duplicates:
#{1 2 3 3} => throws Duplicate key error
; Basically, sets can tell you if they contain a value in O(logN) time.
; Note that order of elements stored in a set are not well-defined.
; Maps are collections of key-value pairs, where the key maps to a value (ala dictionaries).
; Here is a map with three key-value pairs, so key `:name` maps to the string "Ty", `:age` to 40, etc.:
{:name "Ty"
:age 40
:address {:city "Cape Town"
:street "Geneva Dr"}}
; Maps are surrounded by curly brackets.
; Maps can nest and values can be heterogeneous (of different types).
; Keys can be anything (not just keywords).
; All data structures nest and heterogenous, i.e. they can contain values of different types:
[1 "hi" 3 {:age 40}]
=> [1 "hi" 3 {:age 40}]
; (that's a vector with four elements: 1, the string "hi", 3 and a map with one key-value pair where :age maps to 40.
; The `conj` function adds a value to any data structure, e.g.:
(conj [1 2 3] 4)
=> [1 2 3 4] ; note how vectors grow at the back.
(conj '(1 2 3) 4)
=> (4 1 2 3) ; note how lists grow at the front
; `def` is a special form that binds a value to a symbol in the current namespace (like `var x = ...` in JS).
; Let's bind a simple map to symbol `person` using `def`:
(def person {:name "Ty"
:age 40
:address {:city "Cape Town"
:street "Geneva Dr"}})
; Clojure uses prefix notation, so the operator goes first, followed by arguments.
; Forms nest, e.g.
(* 10 2 (+ 1 2))
=> 60
; `get` is a function that gets things out of any data structure,
; e.g. given a key, it can pull a value out of a map:
(get person :name "default")
=> "Ty"
(get person :last-name)
=> nil ; nil because :last-name is not a key present in the map.
(get person :last-name "default")
=> "default" ; get supports an optional default value in 3rd argument
; but keywords also implement the IFn interface, so you can call them as functions to act like `get`:
(:name person)
=> "Ty"
; Higher-Order Functions
; ======================
; Higher-order functions are functions that accept or return other functions, so you can compose functions.
; E.g. the `map` function (distinct from map data structures) "maps over" (or loops over) a collection,
; and returns a lazy list of apply a function to each element
; Let's increment all elements in a vector with the `inc` function:
(map inc [1 2 3])
=> (2 3 4) ; note how `map` returns a list, because it's lazy (not eager, i.e. only produces as many values as the consumer wants.
; `apply` feeds a collection of values as arguments to a function:
(apply + [1 2 3])
=> 6
; is equivalent to
(+ 1 2 3)
=> 6
; `filter` is like `map` but takes a predicate function and only emits values for which the predicate is truthy, e.g.
(filter even? [1 2 3 4 5 6])
=> (2 4 6)
; We can filter any collection, e.g. a list returned from the `range` function:
(filter even? (range 10))
=> (0 2 4 6 8)
; To add up all the _even numbers_ between 0–9, we have to use `apply` because `filter` returns a list:
(apply + (filter even? (range 10)))
=> 20
; Defining Functions
; ==================
; Functions are defined using the `fn` special form, e.g. to define a function that takes one argument `x`, and doubles it:
(fn [x] (+ x x))
=> #object [ztx.scratch$eval104414$fn__104415 0x7ef60335 "ztx.scratch$eval104414$fn__104415@7ef60335"]
; ...returns an instance of a function. But how do we call it?
; Recall the Operational Form, so we need to wrap it in a list:
((fn [x] (+ x x)) 10)
=> 20
; To reuse this function, we can bind it a symbol named `dbl` using `def`:
(def dbl (fn [x] (+ x x)))
; Now we can call the `dbl` function like so:
(dbl 30)
=> 60
; Defining functions are common, so there is a macro called `defn` to save typing:
(defn dbl [x] (+ x x))
; `defn` is a macro that expands to `(def dbl (fn [x] ...))` before it is evaluated:
(macroexpand '(defn dbl [x] (+ x x)))
=> (def dbl (clojure.core/fn ([x] (+ x x))))
; Macros are like functions that alter forms which represent functions before they are evaluated.
; A common macro is the `->>` (thread-last`) macro, which "threads" a pipeline of forms into a nested form, e.g.:
(->> (range 10)
(filter even?)
(map (fn [x] (* x 3)))
(apply +))
; is equivalent to
(apply + (map (fn [x] (* x 3)) (filter even? (range 10))))
; We can inspect this by passing the quoted form to `macroexpand`:
(macroexpand '(->> (range 10)
(filter even?)
(map (fn [x] (* x 3)))
(apply +)))
=> (apply + (map (fn [x] (* x 3)) (filter even? (range 10))))
; Special Forms
; There are only 13 special forms, or built-ins.
; Everything is built out of these special forms:
'[fn let loop do while . def try recur catch throw monitor-enter monitor-exit]
; Example Invoicing App:
(def invoice1 {:invoice/number "INV123"
:invoice/lines [{:qty 10
:description "Design"
:unit-price 1200M}
{:qty 1
:description "Adobe"
:unit-price 100M}]})
(defn calc-line-total
[{:as line
:keys [qty unit-price]}]
; todo: tax
(* 1.15M (* qty unit-price)))
(defn calc-invoice-total [invoice]
(apply + (map calc-line-total (:invoice/lines invoice))))
(calc-invoice-total invoice1)
=> 13915.00M
; Atomic Primitives (non-composites)
nil ; nil means nothing, nada. (null in Java)
foo ; symbols resolve to something else, e.g. `+` resolves to the plus function
123 ; integer (Long in Java)
456.78 ; floating-point number (Float in Java). fast, but imprecise
12345.67M ; decimals have arbitrary-precision, use for money. (BigDecimal in Java)
"hello" ; strings (String in Java)
:age ; keywords are symbol that resolve to themselves. Useful as keys in maps.
; Floating-point inaccuracy (not unique to Clojure), e.g.
(+ 0.1 0.2)
=> 0.30000000000000004 ; floating point inaccuracy
; whereas decimals are accurate:
(+ 0.1M 0.2M)
=> 0.3M
; Special Forms
; There are 13 Special Forms in Clojure:
'[if let loop recur do def . quote try catch monitor-enter monitor-exit]
; everything else is built on top of these special forms, including `and` and `or` logic,
; which are just macros built on top of `if` & `let`.
; e.g. `and` returns if all arguments are truthy, otherwise false:
(and true false true)
=> false
; but if we inspect the `and` macro with `macroexpand`, we see that it expands to a sequence of let & if statements:
(macroexpand '(and true false true))
=> (let* [and__5600__auto__ true]
(if and__5600__auto__
(clojure.core/and false true)
and__5600__auto__))
; it only needs to find the first non-truthy value to return false.
; same is true for `or`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment