This document is currently in alpha test.
If you review it, could you please let me know:
- How long it takes you to read (my current guess is 40 mins)
- What worked well for you. What puzzled you. What jarred. What is overexplained. What is underexplained. etc.
It is not yet completely polished, but it is getting close. It does not yet have an integrated inline/on-page REPL yet. But it will soon.
Please open an issue with your thoughts at the re-freame repo or via twitter here
Many Thanks!
Are you new to ClojureScript? This page will teach you to read Clojure in 40 minutes.
The goal is to teach you just enough ClojureScript to read Reagent
and re-frame
code. The next step is
learning to write ClojureScript but that's a larger skill, which will require more than 40 mins.
Why?¶
Clojure is a modern LISP.
Alan Kay once described LISP as "Maxwell's equations of software". Paul Graham believes LISP was a competitive advantage for his startup. Eric Raymmond said that learning LISP was profoundly enligthening and that knowing it would make you a better programmer for the rest of your days.
In the 70s, 80s and 90s, the LISP community went through a washing machine phase of evolutionary churn. Innovation flourished, experiments happened, prototype ideas were tested and knowledge foliated. Later, in about 2010, Rich Hickey took that knowledge and created Clojure, introducing key innovations. He did this well away from academia (LISP's traditional home) because he wanted to create a pragmatic language for commercial developers.
So, Clojure has had a long gestation period, and it comes with a hard practical edge.
As the owner of a company that develops software products, I believe ClojureScript gives us a competitive edge. As an experienced programmer, I feel like it has provided me with excellent, nourishing brain food. It is a stable, productive place.
If you'll allow me some momentary negativity: in contrast, I'm scared of the javascript landscape, which still appears to be a churning washing machine full of steep learning curves, premature obsolescence and sharp edges, which teach me little of substance and certainly won't make me productive.
Enough of a sermon? Yes. Okay, 40 mins ...
A note on names:
Clojure
runs on the JVM.
ClojureScript
runs in the browser.
They are essentially the same language but our focus here is onClojureScript
.
I have good news: you are going to be surprised and delighted at the simplicity of the syntax.
Simple Data Literals¶
Type | Example | Comment |
---|---|---|
character | \a |
The letter a |
number | 1 |
|
number | 3.14 |
|
string | "hello" |
|
nil | nil |
aka null, None |
Collection Data Literals¶
Lists use ()
, vectors use []
, hashmaps use {}
and sets use #{}
,
Examples:
Collection | Example | Comment |
---|---|---|
list | (1 2 3) |
Empty list is () No commas necessary. But, if present, treated as whitespace. |
list | (1 2 "yes") |
Can be heterogeneous. |
vector | [1 2 3] |
Again, no commas necessary. Empty vector is [] |
vector | [1 "hello" \x nil] |
Can be heterogeneous. |
hashmap | {1 "Hello"} |
A dictionary, or hashmap. Each pair is a key and a value .One pair in this example. 1 is key, "hello" is the value |
hashmap | {1 "Hello" 2 nil} |
No delimiter necessary between pairs. Two key/value pairs in this example. |
hashmap | {1 "Hello" \b 27 "pi" 3.14} |
Can be heterogeneous. Still no commas between pairs. |
set | #{1 "Hello" \a} |
Can be heterogeneous. Still no commas between pairs. |
Collections can nest:
Example | Comment | |
---|---|---|
vector | [1 [2 2] 3] |
an element can be another vector |
vector | [1 {4 5} 3] |
an element can be a hashmap |
hashmap | {1 [4 5] 2 [\a \b]} |
a value in a hashmap can be a vector |
hashmap | {1 {4 5} 2 {\a \b}} |
a value in a hashmap can be a hashmap |
hashmap | {{4 5} 1 [1 2] 2} |
the keys in a hashmap can be a hashmap or vector |
Hashmaps are often just called maps.
Symbols¶
A symbol is a name
that is bound to a value
. Here, we mean "bound" in the sense of "is tied to" or "is associated with" or "refers to".
Type | Example | Comment |
---|---|---|
symbol | inc |
The symbol inc is bound to one of Clojure's builtin functions.This function will return its argument incremented |
symbol | + |
The symbol + is also bound to a builtin function. It adds its arguments.note: + is not an operator in ClojureScript, it is a function |
symbol | yours |
Soon you'll see how you can create a symbol and bind it to a value. But that is a few minutes away down the page, so until then, you'll just have to take it on trust that this is possible/easy. |
That's It For Syntax¶
We've now covered most of Clojure's syntax.
But, how can that be? Haven't we only looked at data literals? Well, yes, but
Clojure is Homoiconic
which means "code is data" - you write Clojure code
using Clojure's data literals and we've covered data literals.
Evaluation¶
You are going to be surprised and delighted with the simplicity of "evaluation".
1st Evaluation Rule: all data litterals other than lists
and symbols
evaluate to themselves.
Value | Evaluates To | Comment |
---|---|---|
\a |
\a |
Same |
1 |
1 |
Same |
[1 1] |
[1 1] |
Same |
{1 1 2 2} |
{1 1 2 2} |
Yep, same |
Below, you can start evaluating live on this page. Click the Eval button, or change the expression, and it will be evaluated. The result of the evaluation will be shown in the box below the editor.
Try these experiments:
- XXX other basic examples?
So, let's now talk about the two exceptions lists
and symbols
...
Evaluating Symbols¶
2nd Evaluation Rule: Symbols don't evaluate to themselves. Instead, they evaluate to the value to which they are "bound".
Example symbol evaluations:
Expression | Evaluates To | Comments |
---|---|---|
foo |
4 |
Assuming the symbol foo is bound to the value 4 |
bar |
[1 2 3] |
Assuming the symbol bar is bound to the value [1 2 3] |
[1 foo] |
[1 4] |
Each element is evaluated, and foo evaluated to 4 |
[foo bar] |
[4 [1 2 3]] |
Assuming foo evaluates to 4 , bar evaluates to [1 2 3] |
Symbols are often bound to functions
For the moment, you'll have to take my word on this: the symbol inc
is bound to a function in Clojure's standard library. As a result, when you evaulate inc
, you get the function!! Because that's what inc
is bound to.
There's also another symbol, count
, which is bound to different function in the standard library.
And, finally - this one will be a surprise - the symbol +
is bound to a function.
Wait. Isn't +
an operator? No. Clojure doesn't have operators.
Instead, +
is a symbol, and it is bound to a function - one which adds. Anyway, more soon.
Try these experiments:
Evaluating Lists¶
3rd Evaluation Rule: a list
evaluates to a function call.
Okay, now we're cooking with gas! Clojure is a functional language so, as you can imagine, function calls are a big deal, so this section is important.
Here's an example list (f arg1 arg2 arg3)
. Because it is surrounded by parens, ( )
, it is a list, and we can see it has four elements.
Such a list is evaluated in two steps:
- first, each element in the list is evaluated (all four of them in this example)
- then, a function call occurs where:
- the evaluation of the 1st element will be the function called
- the evaluation of the other elements (3 in the case above) will be the actual arguments in this function call
The list evaluates to the return value of this function call. So, if this example function call returned the string "maybe Satoshi"
, then the list (f arg1 arg2 arg3)
would evaluate to "maybe Satoshi"
.
More on symbols bound to functions
In Clojure code, the 1st element of a list is often a symbol. So, let's talk more about that.
Here's an example: (inc 3)
. That's a two element list, and the first element is the symbol inc
. The 2nd element is the value 3
When evaluating this list, in step 1 all elements of the list are evaluated and, if the first element of the list is a symbol, like inc
, it will evaluate to what it is bound to, which
is a function. And it is that function which is called in step 2.
So inc
is a symbol, not a function. But it is bound to a function. A subtle but important distinction.
If, instead, inc
was bound to the value 27
we couldn't use it in the first position of a list, because it is bound to a number, not a function. We'd get an error at call time.
Example list evaluations, involving symbols:
List | Evaluates To | Comments |
---|---|---|
(inc 3) |
4 |
Step 1: evaluate each element of the list (there are two): • the symbol inc is bound to a builtin function• and 3 evaluates to 3 Step 2: call the function with 3 as the argument.The return value 4 |
[5 (inc 6)] |
[5 7] |
Evaluate each element of the vector. 5 evaluates to 5 .Then, evaluating that 2nd element is a function call. |
(+ 1 2) |
3 |
The symbol + is bound to a builin function.And that function adds together its arguments |
(+ 1 2 3) |
6 |
Surprise! Turns out this function can handle a variable number of arguments. |
[1 (+ 1 2 3)] |
[1 6] |
No operators
We now know that +
is a symbol bound to a builtin function, not an operator.
It is the same with -
, /
, >
, =
, etc.
Because these are just names, you can also have not=
And because there are no operators, there's no operator precedence to discuss. Simple syntax, right?
Let's start evaluating, live. Type into the following editor. Click Ctrl-click to evaluate. Possible experiments:
- is
(+ 1)
an error? How about(+)
or(*)
? - try
(odd? 5)
- try
(count [1 2 3])
Forms¶
A list like this (+ 1 2)
is known as a form.
Forms can nest like this: (+ 1 (inc 10))
. So, how do nested forms evaluate?
Well, in your mind's eye, see this nested example as (f arg1 arg2)
where:
f
is+
arg1
is1
arg2
is(inc 10)
<-- a nested form
To evaluate this form, you'll remember from the last section that it is a two step process. First, evaluate each of the three elements. So the execution trace will be:
+
will evaluate to the builtin function bound to that symbol1
evaluates to1
(inc 10)
is a list and we evaluate it as a function call: (1) evaluate the elements (2) perform the function call- with all three elements evaluated, the function (bound to
+
) is called with the two actual arguments of1
and11
- the return value from the function call is
12
- which means the overall evaluation of this nested form is
12
More:
Nested Forms | Evaluates To | Comments |
---|---|---|
(+ 3 (count [1 2])) |
5 |
Evaluation trace: • + is evaluated to a function• 3 is evaluated to 3 • (count [1 2]) is evaluated as a function call which returns 2 • call function with args 3 and 2 , which returns 5 |
(= 2 (inc 1)) |
true |
= is a symbol which is bound to a builtin function.You can guess what it does. |
(= (inc 1) (dec 3)) |
true |
dec is bound to a bultin, and it decrements its arg by one |
Evaluate these experiments yourself (any surprises?):
- (inc (dec 1))
- (odd? (inc (dec 1)))
- (= (inc (dec 1)) 1)
Keywords¶
Keywords are like symbols, except they evaluate to themselves and not to a bound value. This means they evaluate like most other data literals.
Keywords are invaluable as identities
and they are used widely, particularly as keys in hashmaps.
A keyword is a name that starts with a colon. Let's evaluate some:
Value | Evaluates To | Comments |
---|---|---|
:foo |
:foo |
It evaluates to itself. |
:bar |
:bar |
It evaluates to itself |
(= :bar :bar) |
true |
Different instances will evaluate to equal. Like numbers do, and strings. |
(= :bar :foo) |
false |
|
[1 2 :bar] |
[1 2 :bar] |
Yep, evaluates to itself. |
{1 :bar} |
{1 :bar} |
And again, but as a hashmap value |
Keywords can have a namespace
.
Value | Evaluates To | Comments |
---|---|---|
:panel1/edit |
:panel1/edit |
Starts with a colon Optionally, contains a / Before the / is the namespace After the / is the name Evaluates to itself |
:panel2/edit |
:panel2/edit |
Different namespace to above, same name |
:panel.commands/edit |
:panel.commands/edit |
namespaces can be dotted |
(name :a/b) |
"b" |
name is bound to a builtin function |
(namespace :a/b) |
"a" |
namespace is bound to a builtin function |
(keyword "a/b") |
:a/b |
keyword is bound to a builtin function |
(keyword "a" "b") |
:a/b |
keyword is bound to a builtin function |
To give you a taste of where this can go, here they are used as the keys in a hashmap:
{:user/id 1
:user/name "Barry"
:user/age 28
:user/company "SpaceX"
:role/name "Rocket Sharpener"}
Evaluate these experiments yourself (any surprises?):
Kebab Case Please¶
We don't use _
in names. Instead we use -
. This applies to both symbols and keywords.
That leads to Kebab Casing like this :the-winner-is
, and not Snake Casing :dont_do_this
.
Pascal case is reserved for a situation which we don't cover in this tutorial.
We can use -
in names because it isn't an operator.
In the form (- 3 2)
, -
is a one character name which is the ultimate in kebab case.
It also happens to look like the minus operator in other languages.
Predicates¶
On that subject, you'll often see Clojure names which include a trailing ?
. For example, even?
or nil?
.
This is a naming convention. It is used for symbols bound to predicate functions which test for a truth.
Form | Evaluates To |
---|---|
(odd? 5) |
true |
(even? 5) |
false |
(nil? 5) |
false |
(nil? nil) |
true |
(empty? []) |
true |
(empty? [:some :thing]) |
false |
Again, we can use ?
in names because it is not
an operator, as it is in other lanugages.
What Have You Learned ?¶
So Far:
- To write Clojure code, you write Clojure data literal. You code in data.
- data can be evaluated, to create new data
- 1st Evaluation Rule: most data evaluates to itself
- 2nd Evaluation Rule:
symbols
evaluate to what they are bound to - 3rd Evaluation Rule:
lists
evaluate to a function call's return value - using "quoting" we can avoid the 2nd and 3rd evaluation rules.
Now, we review some special cases.
Special Forms¶
Some Forms
are special because they evaluate differently to the "normal" rules outlined above. Let's review the important ones.
if
¶
if
forms evaluate in a "special" way.
This (if true 4 3)
is a four element list. Normal evaluation rules
would mean, first, evaluate all four elements of the list, and then calling the if
function with three arguments.
But with if
forms, not all elements are evaluated. The 2nd test
element is evaluated but then either the 3rd or the 4th argument
is evaluated depending on the result of that test
. One element remains unevaluated.
Example | Evaluates To | Comments |
---|---|---|
(if true 4 3) |
4 |
Only true and 4 elements are evaluatedThe 3 element is not evaluated |
(if false 4 3) |
3 |
Only false and 3 elements are evaluated |
(if false 4) |
nil |
else form not provided for evaluation, sothe if evaluates to nil |
(if (odd? 3) 3 4) |
3 |
(odd? 3) evaluates to true |
(= 4 (inc 3)) |
true |
|
(if (= 4 (inc 3)) :t :f) |
:t |
|
[1 (if true "yes")] |
[1 "yes"] |
|
{1 (if false "yes")} |
{1 nil} |
No Statements
Notice how if
is a form in Clojure, which evaluates to a value, and not a statement. (Clojure doesn't have any statements, or operators, it just has data and evaluation rules)
Possible experiments:
- check if
(if true)
is valid. - explore what is "truthy", via
(if "hello" true false)
or(if [] true false)
or(if nil true false)
fn
¶
An fn
form creates a function. We're taking a big step up here.
Here is a very simple example (fn [x] x)
:
- an
fn
form has three elements:- the 2nd is a vector of symbols - in this case
[x]
- the 3rd is a
body
- in this casex
- the 2nd is a vector of symbols - in this case
- an
fn
form is "special" because the 2nd and 3rd elements are notevaluated
- it is only later, when this function is called, that
body
will be evaluated, and when that happens,body
can refer to symbols in the vector, which will be bound to the actual arguments of the function call - our simple example function above only takes one argument. See
[x]
- and, when the
body
is evaluated, at call time, the symbolx
will be bound to the actual argument of that function call - the function will return the evaluation of the
body
, which, in the case above, is the same value as the actual argument. - consequently, if we called this function with the argument
3
, this function would return3
- and, if we called this function with the argument
[:a :b]
, it would return[:a :b]
Question: so, how do we call this function?
Answer: easy, we already know how, just place it as the first element in a form. Then add a 2nd element in that form, which is the actual argument. Like this:
; Aside: a line which starts with a semi-colon is a comment
; The following is a two element list:
; - the 1st is (fn [x] x) and that's a function (created by an `fn` form)
; - the 2nd element is a string "the actual arg"
((fn [x] x) "the actual arg")
I'd like you to see this two element list as more like, say:
(count "the actual arg")
Except, in place of the symbol count
there is a form (fn [x] x)
. count
is symbol which evaluates to a builtin function. Whereas (fn [x] x)
is a form which evaluates to a function. Either way, the first element of the list evaluates to a function and the 2nd element to the list will be the actual argument in this function call.
Let's work through it in more detail:
- This is a two element list, so it will evaluate to a function call, in the two step process described above
- First, evaluate all the elements of the list (remember, there are two)
- The first element (the
fn
form) will evaluate to a function - The second element is a string which will evaluate to itself
"the actual arg"
- The first element (the
- Second, the function call happens
- the function
(fn [x] x)
will be called x
will be bound to the actual argument"the actual arg"
- calling the function means evaluating the body of the function
- The body of the function is just
x
, a symbol which is bound to the actual argument"the actual arg"
- So the body evaluates to
"the actual arg"
- the function
- The functions return value will be
"the actual arg"
- And that then is the evaluation of the entire form
Here's another call to the same function:
((fn [x] x) [:a :b])
The actual argument is [:a :b]
, and the form (function call) will evaluate to [:a :b]
.
Let's create a different function and evaluate it:
((fn [num] (+ num 1)) 4)
The body of the function, (+ num 1)
, will be evaluated with the num
symbol bound to the actual parameter 4
. The return value is 5
.
Try these experiments:
what if, instead, we called this function with false
.
You won't see this written in normal ClojureScript code, because it is weird, but here's a puzzle:
What is the evaluation? Note: there is an extra set of parens around the fn
form.
def
¶
The def
form creates a symbol and binds it to a value.
(def gurus 2)
This defines the symbol gurus
and binds it to the value 2
. If, later, we used this symbol in other code, it would evaluate to 2
.
def
is a "special form" in two ways:
- when evaluated, it adds to the global set of defined symbols. Such mutation is known as a side-effect. Functions don't normally cause side effect - they are normally pure.
- in a normal Clojure form, like say
(f gurus 2)
, thegurus
element would be evaluated before the call. But the evaluation rules in adef
form are different because, instead,gurus
is the symbol to define. It doesn't get evaluated. But the 3rd element certainly does.
The 3rd element of a def
is evaluated:
(def saints (inc gurus)) ;; look, using the symbol `gurus` !!
saints
is now also a defined symbol and it is bound to the evaluation of (inc gurus)
, which is (inc 2)
, which is 3
.
Consider these two:
(def beach-list [:hat :sunglasses :towel])
(def beach-items (count beach-list)) ;; count is a builtin function
beach-items
is a symbol bound to 3
Now, consider what is happening here:
(def my-inc (fn [a] (+ a 1))
That fn
form will create a function, and then the def
will bind it to the symbol my-inc
. Two steps. Hey, we've just created our own inc
.
(my-inc 4)
evaluates to 5
And again:
(def square-it (fn [x] (* x x)))
We can use this symbol square-it
in a form (it is now bound to a function), like this:
(square-it 5)
evaluates to 25
defn
¶
A ClojureScript program typically contains a lot of function definitions, and combining
def
and fn
each time would be verbose. So, there is a shorter way
which combines the two, called defn
.
You use it like this:
(defn dec ; `dec` is the symbol being defined
[n] ; a vector of symbols for the actual arguments
(- n 1)) ; function body - evaluated when function is called - uses `n`
So, this binds a symbol to a function. All of the "builtin functions" mentioned previously in this tutorial are defined this way: str
, count
, inc
, namespace
, +
etc.
To use the symbol/function just defined:
(dec 4)
evaluates to 3
Define another function:
(defn square-it ; `square-it` is the symbol being defined
[n] ; a vector of symbols for the actual arguments
(* n n)) ; function body - evaluated when function is called - uses `n`
use it:
(square-it 3)
evaluates to 9
.
Or use it like this:
(square-it (inc 3))
evaluates to 16
.
Define another:
(defn greet
[username]
(str "Hello " username)))
str
is a builtin function which turns each of its arguments into a string and concatenates them.
use it:
(greet "world")
evaluates to "Hello world"
.
In a functional language, creating functions is a big deal. You'll be using defn
a lot.
let
¶
This is another special form you'll be using a lot.
; a let form has two parts:
; - 1st a vector of "binding pairs"
; - 2nd a "body"
(let [a "pen"] ; a pair - binds the symbol `a` to the value "pen"
a) ; the body is evaluated with the bindings
This let
form evaluates to "pen"
Another one:
(let [a "pen"]
:foo) ; odd. The body doesn't use `a`. But this still works.
This let
form evaluates to :foo
Notice the way this let
form is formatted:
(let [a "pen" ; this pair means `a` is bound to "pen"
b "sword"] ; this pair means `b` is bound to "sword"
(> a b)) ; is "pen" greater than "sword" ??
evaluates to false
. Wait, no. That isn't conventional wisdom. Damn you lexicographic comparisons!!
We need to make some changes:
(let [a "the pen"
b "a sword"]
(if (> a b) a b))
evaluates to "the pen"
. Phew!
(let [a "the pen"
b "a sword"]
{:winner-is (if (> a b) a b)})
evaluates to {:winner-is "the pen"}
let
is often used within a defn
:
XXX experiment with greet
In this particular, we could have got away with no using a let
, like this:
(defn greet
[name friendly?]
[:div (if friendly? "Hello " "Go away ") name)
(defn items-text
[items]
(let [num (count items)
plural (if (= num 1) "" "s")
verb (if (> num 1) "are " "is ")]
(str "There " verb num " item" plural)))
Check on the output in these cases:
(items-text [:towel :sunglasses])
(items-text [:towel])
(items-text [])
Exercise:
- when there are no items make the text "there are no items" (and not "there is 0 items")
What have You Learned ?¶
So Far:
- the syntax is just data literals
- data can be evaluated
- lists (forms) evaluate to function calls
- there are special forms (exceptions to the normal evaluation rules for forms)
defn
allows us to bind a function to a symbol
Data Functions¶
Sometimes data can act as a function - it can appear as the first element in a form.
For example, a hashmap can act as a function. This is easiest to explain via code:
({:a 11} :a)
Here, a hashmap {:a 11}
is used as the 1st element in a form
. The actual argument, :a
, will be looked up in that hashmap, and the associated value, 11
, is the return value of the function call. So 11
is the evaluation.
({:a 11 :b 21} :b)
evaluates to 21
. We are looking up the key :b
in the hashmap {:a 11 :b 21}
and obtaining the associated value 21
({:a 11 :b 21} :c)
evaluates to nil
. There's no key :c
in the hashmap.
({:a 11 :b 21} :c :not-found)
evaluates to :not-found
. The key was not found in the hashmap, but we supplied a default value as the 2nd actual argument (the 3rd element of the list).
This can be reversed. A keyword
can also be used as the 1st element in a form, provided the actual argument is a hashmap.
(:a {:a 11})
evaluates to 11
(:b {:a 11 :b 21})
evaluates to 21
(:c {:a 11 :b 21})
evaluates to nil
(:c {:a 11 :b 21} :not-found)
evaluates to :not-found
This approach of using a keyword as the function, with a hashmap argument happens a lot in ClojureScript Code.
BuiltIn Functions¶
Clojure's has a substantial library of builtin functions. A few in particular are used all the time, and to read Clojure code (our aim here), you must know them.
assoc
¶
assoc
allows you to add a key/value pair to a hashmap.
Example | Evaluates To | Comments |
---|---|---|
(assoc {} :a 4) |
{:a 4} |
adding a key/value pair to the empty hashmap {} |
(assoc nil :a 4) |
{:a 4} |
nil is treated as {} |
(assoc {:b 1} :a 4) |
{:b 1 :a 4} |
adding a key/value pair to the hashmap {:b 1} |
(assoc {} :b 1 :a 4) |
{:b 1 :a 4} |
add two pairs at once |
map
¶
Applies a function to all the elements of a collection.
First, we create a function called plus-one
(defn plus-one
[it]
(+ 1 it))
Now use it with map
:
Example of map | Evaluates To | Comments |
---|---|---|
(map plus-one [1 2 3]) |
(2 3 4) |
Applies plus-one to each element of the collection.Think [(plus-one 1) (plus-one 2) (plus-one 3)] except the result is a list, not a vector. |
(map inc [1 2 3]) |
(2 3 4) |
Same as above, but now inc |
(map inc '(1 2 3)) |
(2 3 4) |
Same as above. But now across a list .Note the use of quote on the list |
(map inc #{1 2 3}) |
(2 3 4) |
Same as above. But now across a set . |
(map count ["hi" "world"]) |
(2 5) |
Think (count "hi") (count "world") |
(map :a [{:a 1} {:a 11}]) |
(1 11) |
Earlier, we learned that keywords can act likefunctions. Think (:a {:a 1}) (:a {:a 11}) |
Note:
map
always returns alist
, even if you give it a vector, set, list, etc.
Create add
for use with map
below:
(defn add
[a b]
(+ a b))
Example | Evaluates To | Comments |
---|---|---|
(map add [1 2 3] [4 5 6]) |
(5 7 9) |
Applies add to pairs of elements.Think [(add 1 4) (add 2 5) (add 3 6)] , exceptresult is a list |
(map + [1 2 3] [4 5 6]) |
(5 7 9) |
Same as above. But using the builtin function + |
Exercises:
(map {:a 1 :b 2 :c 3} [:a :b :c :d])
What would this evaluate to:
(map count (map str (map inc [9 99 999])))
XXX Live coding here.
reduce
¶
In the functional world, reduce
is part 600 pound gorilla, part swiss arm knife.
It takes three arguments:
- a function which "accumulates"
- the initial state of the accumulation
- a collection to accumulate "across"
(reduce + ;; accumulating with `+`
0 ;; initial state of accumulation
[1 2 4]) ;; the collection to accumulate across, element by element
The accumulation function, +
in the example above, must accept two arguments:
- the current, running accumulation, up to the current element
- the new element to process
The accumulation function should combine the current, running accumulation, with the new element, and return an updated accumulation. +
works because it can take two arguments and it will return a new accumulation of the two.
Because there are three elements in our example collection [1 2 4]
, there are three steps in the reduction. If our collection had 100 elements, there would be 100 steps in the reduction.
This is effectively what reduce will compute for the example above:
(+ (+ (+ 0 1) 2) 4)
evaluates to 7
and, in this case reduce
, is effectively summing the collection
Or, to explain the three steps another way:
Collection Element | Running Accumulation |
Evaluation using + |
New Accumulation | Comment |
---|---|---|---|---|
1 |
0 |
(+ 0 1) |
1 |
+ is applied to the initial value 0 and the 1st element of the collection 1 , producing a new accumulation of 1 |
2 |
1 |
(+ 1 2) |
3 |
+ is applied to the accumulation 1 and the 2nd element of the collection 2 , producing a new accumulation of 3 |
4 |
3 |
(+ 3 4) |
7 |
+ is applied to the accumulation 3 and the 3rd element of the collection 4 , producing a new accumulation of 7 |
Next example: we create this accumulation function:
(defn max
[a b]
(if (> a b) a b))
Then we use that function with a reduce:
(reduce max ;; accumulating with `max`
0 ;; initial accumulation (maximum)
[1 2 4]) ;; the collection to process
Effectively, what gets evaluated:
(max (max (max 0 1) 2) 4)
How about this one:
(reduce str ;; accumulating with the builtin `str`
0
[1 2 4])
We "accumulate" a string
How about this one:
(reduce conj ;; <--- `conj` is a builtin too
[] ;; <--- empty vector
[1 2 4])
Or this one:
(defn only-odd
[accumulation val] ;; <-- accumulated value, new value
(if (even? val)
accumulation
(conj accumulation val)))
(reduce only-odd
[] ;; <--- start with an empty vector
[1 2 3 4 5 6])
The Arrow Macros¶
Clojure has an advanced feature called macros
which we'll review now,
because you need to understand their impact.
Macros are functions which are run at compile time, not run time. Their job is to rewrite your code in useful ways. This is probably not a feature you'll have experienced before in other languages.
Wait, rewrite my code? How is that even possible?
Well, in ClojureScript your code is data. It is just arrangements of data literals put into text file, involving mostly lists, symbols and vectors. This property means a function can take your code (which is just data, remember), and compute new data, which is new code. And that is how a macro rewrites your code - it is a function that runs at compile time.
The key takeaway is that macros rewrite your code in useful ways.
Thread First Macro ->
¶
Let's talk examples ... here's a deeply nested arrangement of forms we talked about earlier:
(+ (+ (+ 0 1) 2) 4)
Deeply nested forms, like this, are often rewritten using the thread first
macro ->
:
(-> (+ 0 1) ; (+ 0 1)
(+ 2) ; (+ (+ 0 1) 2)
(+ 4))) ; (+ (+ (+ 0 1) 2) 4)
->
is a symbol and it is the first element in a form (-> ...)
.
So, this is a function call and the symbol ->
is bound to the function to be called, a bit
like (+ ...)
. And, in this example, the function call has three actual arguments - I can see three forms.
But this ->
function is actually getting called at compile time, not run time.
And this thread first
function (marco) will rewrite/reorganise the three forms provided as arguments, producing new code. And that new code will match exactly the deeply-nested-forms version given above.
Read the thread first
version (above) like this:
- First,
(max 0 1)
is evaluated to1
- Then that
1
value becomes the 1st argument to the next form down, which is(+ 2)
, effectively creating(+ 1 2)
- Then the evaluation of that form becomes the first argument to the next form, which is
(+ 4)
, effectively creating(+ 3 4)
- The result is
7
Notice how the value for the previous form's evaluation is always "threaded" into the form below as the 1st argument. Hence the name "thread first macro".
Deeply nested forms can be a bit hard to read, so the thread first
macro arrangement allows you to visualise the computation as a cascading data flow.
It is as if the evaluation of the higher form is "flowing down" and into the form underneath. That's a useful mental model for many.
How about this use of ->
:
(-> {}
(assoc :a 1) ; (assoc {} :a 1)
(assoc :b 2)) ; (assoc (assoc {} :a 1) :b 2)
Read that as:
- First,
{}
is evaluated to{}
- Then that value becomes the 1st argument to the next form down creating
(assoc {} :a 1)
- And the evaluation of that form becomes the first argument to the next form creating
(assoc {:a 1} :b 2)
- The entire form evaluates to
{:a 1 :b 2}
Now, we could choose to simply write it the deeply nested way ourselves, of course, but humans seem to better understand the "data flow" version more than the nested forms version.
Work out the evaluation of:
(-> {:a {:aa 11}}
:a ;; same as (:a)
:aa) ;; same as (:aa)
->
belongs to a small family of marcos. Another one is the thread last
macro ->>
. It operates the same way except it threads the value as the argument in the last position, not the first.
Thread Last Macro ->>
¶
Do you remember this nested form example from earlier:
(map count (map str (map inc [9 99 999])))
This can be rewritten using the "thread last" macro ->>
:
(->> [9 99 999]
(map inc) ; (map inc [9 99 999])
(map str) ; (map str (map inc [9 99 999]))
(map count)) ; (map count (map str (map inc [9 99 999])))
With thread last
the evaluation of the previous form is threaded into the next form as the last argument (not the first).
Work out the evaluation of:
(->> (range 10) ; (range 10)
(map inc) ; (map inc (range 10))
(filter odd?)) ; (filter odd? (map inc (range 10))
The Hard Bit¶
The learning curve so far as been gentle. Clojure has simple syntax and simple evaluation semantics.
The steeper part of the Clojure learning curve, and the part that takes most of the time to master, is figuring out how to write code using pure functions and immutable data. If you have previously only used imperative, place-oriented languages (eg. OO languages), this paradigm change takes time to click.
But, of course, the purpose of this tutorial is to teach you to read Clojure, which is an easier skill. So, onward ...
Immutable Data¶
ClojureScript uses immutable data structures by default.
The rule: once you create data, you can't mutate it (change it). Ever.
But you can create a revision (a copy) of that data, which is modified in some way. The original data is untouched. But, then, you can't change this revision either. But, you can create a further revision (copy) of the revision, etc.
Let's see this in action. Evaluate the following:
(let [car1 {:doors 2} ; an original hashmap
car2 (assoc car1 :seats 4) ; add a key/value pair, a new revision is created
car3 (assoc car2 :engine :big)] ; add a key/value pair, a new revision is created
[car1 car2 car3]) ; the value associated with car1 is untouched
you'll see a vector of three values [{:doors 2} {:doors 2, :seats 4} {:doors 2, :seats 4, :engine :big} ]
.
Notice how car1
is unchanged, even though we did an assoc
into car1
. Same with XXXXXXXXX
When you are used to imperative, in-place modification of data, it can initially be mysterfying as to how you can achieve anything. Rest assured, you can.
More experiments. If we evaluate this:
(assoc (assoc score :c 3) :d 4)
there will be three revisions of a hashmap. The original one bound to score
which is {:a 1 :b 2}
. Then there's the one which results from (assoc score :c 3)
. And then there is the final one {:a 1 :b 2 :c 3 :d 4}
.
If you are new to Immutable Data, you probably have two concerns:
- Surely this is inefficient? Don't worry, via clever algorithms, efficiency is seldom an issue.
- How do you get anything done? (Don't worry, there are answers here too).
Using Immutable data dovetails with pure functions, to reduce cognitive load and bring a great simplicity to your programming.
All grand claims. We'll need to see how this plays out in bigger programs.
Evaluate the following:
(let [car1 {:doors 2} ;; original hashmap
car2 (assoc car1 :doors 4) ;; new value for :doors
car3 (assoc car2 :doors 2)] ;; back to the original value of 2
[(= car1 car3) (identical? car1 car3)]) ;; identical? tests if two pieces of data at the same piece
Evaluate the following:
(let [car1 {:doors 2} ;; this hashmap can't be changed
car2 (assoc car1 :doors 2)] ;; wait on, same values!!!
(identical? car1 car2)) ;; identical? tests if two pieces of data at the same
XXX boolean and and or ??? https://j19sch.github.io/blog/clj3-and-or-being-weird/
XXX destructuring ?? Later
XXX interop ?? Mention ??
XXX Reagent like code ?? Next tutorial
XXX atoms and deref - defer this to later
XXX tracking is reified dynamics. residential
; short-hand for creating a simple function: ; #(...) => (fn [args] (...))
( 3 %) ; => (fn [x] ( 3 x))¶
( 3 (+ %1 %2)) ; => (fn [x y] ( 3 (+ x y)))¶
Summary¶
We have learned:
- in ClojureScript we evaulate data, to create new data
- virtually all data literals evaluate to themselves
We've looked at ClojureScript through a lens which makes it easier to understand Reagent and re-frame.
Installing¶
To install Clojure and Leiningen (a build tool) following these instructions.
Here's a good intro on writing a function: https://blog.cleancoder.com/uncle-bob/2020/04/09/ALittleMoreClojure.html
Gently paced video series showing you ClojureScript coding/tooling from the basics up: https://www.youtube.com/watch?v=SljDPNwAFOc&list=PLaGDS2KB3-ArG0WqAytE9GsZgrM-USsZA
The cheatsheet: https://clojure.org/api/cheatsheet
A visual overview of the similarities and differences between ClojureScript and JavaScript https://www.freecodecamp.org/news/here-is-a-quick-overview-of-the-similarities-and-differences-between-clojurescript-and-javascript-c5bd51c5c007/
We haven't covered:
- macros - write functions which take your code/data and manipulate it at compile time.
- JavaScript interop.
(js/console.log "Hello World!")