- 0. Some Context
- 1. "Fundamental" Values
- 2. Compound Values (collections)
- 3. Sequences
- 4. Atoms
- 5. Operators
- 6. Destructuring
Clojure is a dynamic
functional
hosted
compiled
lisp
.
Let's break that down:
- dynamic all the language features are available at runtime
- functional programs are made up of functions that take / return immutable data
- hosted there is no Clojure VM (unlike Java / Python / JS / ...), instead Clojure is supposed to be a programmable layer on top of existing VMs
- compiled Clojure forms are compiled down to the host VM's "machine" language (JVM bytecode / JS) before evaluation
- lisp is a family of programming languages
The most popular implementations of Clojure are:
- Clojure - hosted on the JVM, reference implementation
- ClojureScript - hosted on JS (browser / node.js / ...)
You can evaluate and play with code snippets presented in this document in a ClojureScript REPL (shell).
Try copying (js/alert "Hello, world!")
, and evaluating it in the REPL.
nil
is a value that means "nothing"
true
and false
are boolean values
1
is a value- so is
1.23
- and so is
1/3
Note:
Although ratios are converted to JS Numbers in ClojureScript,
they are fully supported as a special type in Clojure.
"this is a string"
Note:
Although strings are fundamental values in ClojureScript (like in JS),
they are sequences of characters in Clojure (like in Java).
Keywords start with a colon and don't have whitespace
:key
:some-keyword
:a-weird/keyword
In Clojure we prefer keywords over strings to name things:
// JS
{
"a": 1,
"b": "hello!"
}
;; Clojure
{:a 1
:b "hello!"}
- functions are values
- they can
take
a bunch of values as input, andreturn
a value as output - the number of arguments a function takes is called its
arity
(fn [x] (+ x 1))
is a function that- takes a number as its argument
- returns that number + 1
- can also be written as
#(+ % 1)
- can be called in this manner:
(#(+ % 1) 5)
returns6
Try evaluating these at the REPL:
+
unbound-symbol
When Clojure sees a symbol, it:
- tries to find a value bound to it (
+
is bound to a function that performs addition) - complains if it doesn't find one (
unbound-symbol
is not bound to anything)
If we want the symbol itself, not the value it is bound to, we have to quote
it:
(quote +)
(quote unbound-symbol)
There's also a shorthand for quoting:
'+
'unbound-symbol
(def x 1)
- binds the value
1
to the symbolx
in the currentnamespace
(+ x 1)
returns2
,x
remains1
- binds the value
- the form
(def triple (fn [x] (* x 3)))
- binds
(fn [x] (* x 3))
totriple
(triple 2)
returns6
- shorthand:
(defn triple [x] (* x 3))
- binds
The form
(let [a 1
b 2]
(+ a b))
- binds
1
toa
,2
tob
- returns
3
- unbinds
a
andb
A "vector" is:
- ordered
- random-access
Example:
10 at index 0
20 at index 1
30 at index 2
Let's implement this in Clojure!
(def my-vector
(fn [idx]
(case idx
0 10
1 20
2 30)))
Explanation:
(my-vector 2)
;; => (case 2
;; 0 10
;; 1 20
;; 2 30)
;; => 30
The clojure.core
namespace already implements vector
for us:
(def clj-vector
(vector 10 20 30))
;; shorthand
(def clj-vector
[10 20 30])
(clj-vector 2)
;; => 30
A "linked list" is:
- ordered
- sequential access
- made of
cell
s
A "cell" can be modeled as a vector of size 2.
Example:
a web browser session history:
- opening a browser tab
- going to google
- going to google images
can be represented as 3 cells:
C0 ["about:blank" nil]
C1 ["google.com" C0]
C2 ["google.com/images" C1]
Let's implement this in Clojure!
(defn cell [head tail]
[head tail])
(defn head [cell]
(cell 0))
(defn tail [cell]
(cell 1))
(def my-list
(cell "google.com/images" (cell "google.com" (cell "about:blank" nil))))
Explanation:
(head my-list)
;; => (my-list 0)
;; => "google.com/images"
(head (tail my-list))
;; => (head (my-list 1))
;; => ((my-list 1) 0)
;; => "google.com"
(head (tail (tail my-list)))
;; => (head (tail (my-list 1)))
;; => (head ((my-list 1) 1))
;; => (((my-list 1) 1) 0)
;; => "about:blank"
The clojure.core
namespace has functions called cons
, first
, and rest
that are similar to our cell
, head
, and tail
functions.
(def clj-list
(cons 10 (cons 20 (cons 30 nil))))
;; shorthand
(def clj-list
(list 10 20 30))
(first clj-list)
;; => 10
(first (rest clj-list))
;; => 20
(first (rest (rest clj-list)))
;; => 30
(nth clj-list 2)
;; => 30
A "hash-map" is:
- unordered
- random-access
- made up of key-value pairs
Example:
1 at key :a
2 at key "b"
3 at key 40
Let's implement this in Clojure!
(def my-hash-map
(fn [idx]
(case idx
:a 1
"b" 2
40 3
;; default
nil)))
Explanation:
(my-hash-map "b")
;; => (case "b"
;; :a 1
;; "b" 2
;; 40 3)
;; => 2
(my-hash-map :c)
;; => nil
The clojure.core
namespace already implements hash-map
for us:
(def clj-hash-map
(hash-map
:a 1
"b" 2
40 3))
;; shorthand
(def clj-hash-map
{:a 1
"b" 2
40 3)
(clj-hash-map "b")
;; => 2
(clj-hash-map :c)
;; => nil
;; Note: keywords are lookup functions for hash-maps!
(:a clj-hashmap)
;; => 1
A "set" is:
- unordered
- random-access
- made up of value-value (or key-key) pairs
Example:
:a at key :a
"b" at key "b"
40 at key 40
Let's implement this in Clojure!
(def my-set
{:a :a
"b" "b"
40 40})
Explanation:
(my-set 40)
;; => 40
(my-set :hello)
;; => nil
The clojure.core
namespace already implements set
for us:
(def clj-set
(set [:a "b" 40]))
;; shorthand
(def clj-set
#{:a "b" 40})
(clj-set 40)
;; => 40
(clj-set :hello)
;; => nil
- The data structures that we implemented and that Clojure provides cannot be mutated once they have been created.
- All collections are heterogenous, ie items can be of different types:
["a" :b 3 #(* % 5)]
Clojure can convert many types of collections into "sequences".
Try evaluating these at the REPL:
(seq (list 1 2 3))
(seq [1 2 3])
(seq {:a 1 :b 2})
(seq "hello")
A sequence is a "logical list", with a head and a tail.
A more detailed explanation can be found here.
These are the three most important operations that can be performed on sequences.
(map inc (list 1 2 3))
;; => (cons (inc 1) (map inc (list 2 3)))
;; => (cons 2 (cons (inc 2) (map inc (list 3))))
;; => (cons 2 (cons 3 (cons (inc 3) (map inc (list)))))
;; => (cons 2 (cons 3 (cons 4 (list))))
;; => (2 3 4)
(map not [true false])
;; => (false true)
(reduce + (range 5))
;; => (reduce + (list 0 1 2 3 4))
;; => (reduce + 0 (list 1 2 3 4))
;; => (reduce + (+ 0 1) (list 2 3 4))
;; => (reduce + (+ 1 2) (list 3 4))
;; => (reduce + (+ 3 3) (list 4))
;; => (reduce + (+ 6 4) (list))
;; => 10
;; factorial of 5
(reduce * (range 1 (inc 5)))
;; => (reduce * (range 1 6))
;; => (reduce * (list 1 2 3 4 5))
;; => 120
(filter even? (range 5))
;; => (filter even? (list 0 1 2 3 4))
;; => (cons 0 (filter even? (list 1 2 3 4)))
;; => (cons 0 (filter even? (list 2 3 4)))
;; => (cons 0 (cons 2 (filter even? (list 3 4))))
;; => (cons 0 (cons 2 (filter even? (list 4))))
;; => (cons 0 (cons 2 (cons 4 (filter even? (list)))))
;; => (cons 0 (cons 2 (cons 4 (list))))
;; => (0 2 4)
We need to move away from a notion of state as "the content of this memory block" to one of "the value currently associated with this identity". Thus an identity can be in different states at different times, but the state itself doesn’t change. - clojure.org
atom
s are the most common kind of "identities" in Clojure- their state can be a different immutable value at different points in time
- they are thread-safe (on multithreaded platforms)
Let's implement this in Clojure!
(defn atm [init]
#js {:state init})
(defn state [atm]
(.-state atm))
(defn reset [atm val]
(set! (.-state atm)
val))
(defn swap [atm f]
(reset atm
(f (state atm))))
(def my-atom
(atm 0))
Explanation:
my-atom
;; => #js {:state 0}
;; JS object with 'state' property set to 0
(state my-atom)
;; => (.-state #js {:state 0})
;; => 0
(reset my-atom 1)
;; => (set! (.-state my-atom) 1)
(state my-atom)
;; => 1
(swap my-atom inc)
;; => (reset my-atom
;; (inc (.-state my-atom)))
;; => (set! (.-state my-atom)
;; (inc 1))
(state my-atom)
;; => 2
The clojure.core
namespace has functions called atom
, deref
, reset!
, and swap!
corresponding to our atm
, state
, reset
, and swap
functions.
(def clj-atom
(atom 0))
;; get current state
(deref clj-atom)
;; => 0
;; shorthand
@clj-atom
;; => 0
;; reset state
(reset! clj-atom 1)
;; => 1
@clj-atom
;; => 1
;; update state
(swap! clj-atom inc)
;; => 2
@clj-atom
;; => 2
Form | Operator | Operator Type | Operands | Return |
---|---|---|---|---|
(reverse [1 2 3]) |
reverse |
function | [1 2 3] |
[3 2 1] |
(defn same [x] x) |
defn |
macro | same , [x] , x |
(def same (fn [x] x)) |
(if (even? x) "EVEN!" "ODD!") |
if |
special form | (even? x) , "EVEN!" , "ODD!" |
"EVEN!" / "ODD!" |
- are functions that are called at compile time
- take source code as arguments and return source code as output
- used to extend the compiler with new syntax / semantics
- examples:
defn
,or
,->
, etc.
A more detailed explanation can be found here.
- builtin operators known to the compiler
- examples:
def
,if
,try
, etc.
A more detailed explanation can be found here.
We can bind values inside arbitrarily nested collections to symbols using "destructuring":
(let [[one two three] [1 2 3]]
(+ one two three))
;; => 6
(let [[one & others :as nums] [1 2 3]]
[one others nums])
;; => [1 (2 3) [1 2 3]]
(let [{a "a"
:keys [b c]
:as H} {"a" 1 :b 2 :c 3}]
[a b c H])
;; => [1 2 3 {"a" 1, :b 2, :c 3}]
A more detailed explanation can be found here.