Skip to content

Instantly share code, notes, and snippets.

@divs1210
Last active December 25, 2022 02:55
Show Gist options
  • Save divs1210/172f2a964e1ef04f81c63a8ab9d7d599 to your computer and use it in GitHub Desktop.
Save divs1210/172f2a964e1ef04f81c63a8ab9d7d599 to your computer and use it in GitHub Desktop.
A Gentle Introduction to Clojure

Clojure Primer

0. Some Context

About Clojure

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 / ...)

Follow and Explore!

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.

1. "Fundamental" Values

Self Describing

nil

nil is a value that means "nothing"

Booleans

true and false are boolean values

Numbers

  • 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.

Strings

"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

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!"}

Non Self Describing

Functions

  • functions are values
  • they can take a bunch of values as input, and return 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) returns 6

Symbols

Try evaluating these at the REPL:

  • +
  • unbound-symbol

When Clojure sees a symbol, it:

  1. tries to find a value bound to it (+ is bound to a function that performs addition)
  2. 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
Binding values to symbols
def
  • (def x 1)
    • binds the value 1 to the symbol x in the current namespace
    • (+ x 1) returns 2, x remains 1
  • the form
    (def triple
      (fn [x] 
        (* x 3)))
    • binds (fn [x] (* x 3)) to triple
    • (triple 2) returns 6
    • shorthand:
      (defn triple [x]
        (* x 3))
let

The form

(let [a 1
      b 2]
  (+ a b))
  • binds 1 to a, 2 to b
  • returns 3
  • unbinds a and b

2. Compound Values (collections)

Vector

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

List

A "linked list" is:

  • ordered
  • sequential access
  • made of cells

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

Hash-map

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

Set

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

Notes

  1. The data structures that we implemented and that Clojure provides cannot be mutated once they have been created.
  2. All collections are heterogenous, ie items can be of different types: ["a" :b 3 #(* % 5)]

3. Sequences

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.

map / reduce / filter

These are the three most important operations that can be performed on sequences.

map

(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

(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

(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)

4. Atoms

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

  • atoms 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

5. Operators

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!"

Macros

  • 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.

Special Forms

  • builtin operators known to the compiler
  • examples: def, if, try, etc.

A more detailed explanation can be found here.

6. Destructuring

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment