Last active
January 9, 2026 12:16
-
-
Save theronic/b6fa9ef78a0f96acf2b76e48ab0d2674 to your computer and use it in GitHub Desktop.
Clojure Lesson 2026-01-06
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ; 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