Basics of Clojure
- Author: Stephen Ball
- Published:
-
Tags:
- Permalink: /blog/basics-of-clojure
See how Clojure's syntax works, how functions work, and some basic data structures.
Have you seen any Clojure code?
Yes? You’ll almost certainly not see anything new or interesting here.
No? Let’s change that! I’m certainly not an expert: but I’ve been reading “Getting Clojure” by Russ Olsen so I at least know enough to give a quick tour.
Basics
Hello!
(println "Hello World")
Not too weird yet. There’s parentheses around the line but it’s not too far from something like print "Hello World"
.
But here, the following line adds up the integers 1 through 4.
(+ 1 2 3 4) ; 10
If you’ve never seen a Lisp language then this probably looks weird. Clojure is my first Lisp dialect and sure looked strange to me at first. But I was surprised to find how quickly I came to appreciate the clear structure of the parentheses.
As a Lisp dialect, the syntax of Clojure is structured data in parentheses. To make anything happen in Clojure it’s gotta be in parentheses.
Comments in Clojure are prefixed with a semicolon (a double semicolon conventionally for lines that are entirely comments).
The structure of every (as far I have seen) Clojure call
(function arg0 arg1 arg2 arg3 … argN)
With that knowledge in mind we can make sense of that first (+ 1 2 3 4)
line. The +
function is being called with the arguments 1 2 3 4
. The equivalent line In a non-Lisp language would be 1 + 2 + 3 + 4
.
One of the major foundations of Clojure is that everything is done via functions. Arithmetic, variable assignment, logic, conditionals, and even key lookups in a map are done by treating the key as a function.
;; Arithmetic
(+ 1 2 3) ; 6
(/ 6 2) ; 3
(/ 24 2 3) ; 4
;; Variable Assignment
(def greeting "Hello")
;; Logic
(= 2 3) ; false
(= 2 2 2) ; true
(> 9 7 3) ; true
(> 9 4 5) ; false
;; Conditionals
(def a 9)
(def b 3)
(if (> a b) a b) ; 9
;; Map lookup
(def zork1 {:released 1980 :genre :text-adventure})
(:released zork1) ; 1980
(:genre zork1) ; :text-adventure
You’ll notice that a bunch of those functions take more arguments than you may expect. In many languages equality is a two argument expression e.g. a == b
but in Clojure you can compare an arbitrary number of arguments quite expressively.
Also that if
line probably looks quite weird. It returns a if it’s larger than b and b otherwise. (We’ll break that one down more in a sec.)
(if (> a b) a b)
Conditionals
In Clojure conditionals are also functions. The if
function is called with 2 (optionally 3) arguments.
-
If the first argument evaluates to
true
then the second argument is evaluated and its result becomes the result of the entire statement. -
If the first argument evalulates to
false
then the result isnil
OR the optional third argument is evaluated.
user=> (if (= 2 2) "yes 2 equals 2")
"yes 2 equals 2"
user=> (if (= 2 3) "yes 2 equals 2")
nil
Now the if
statement from before can make more sense.
(if (> a b) a b)
; ⌃⌃⌃⌃⌃⌃⌃ ⌃ ⌃
; │ │ │
; └────── first arg: the logic to evaluate
; │ │
; └──── second arg: evaluated if first arg is true
; │
; │
; └── third arg: evaluated if first arg is false
Variables
Defining variables in Clojure is a function as well. The def
function accepts a name and anything that evaluates to a value.
user=> (def number 123)
#'user/number
user=> number
123
user=> (+ number 4)
127
Functions
As a functional language, functions are naturally at the heart of Clojure. They can be defined with the fn
function which accepts a vector (basically an array) of arguments followed by statements to evaluate. The last statement’s result will be the returned result of calling the function.
(fn [n] (* 2 n))
We can assign that function to a name with def
user=> (def doubler (fn [n] (* 2 n)))
#'user/doubler
user=> (doubler 3)
6
user=> (def printy-doubler (fn [n] (println "Doubling" n) (* 2 n)))
#'user/printy-doubler
user=> (printy-doubler 3)
Doubling 3
6
Using def
and fn
works but is not great experience that Clojure wants its programmers to have. Clojure provides the defn
function to easily name and define a function in one call.
user=> (defn tripler [n] (* 3 n))
#'user/tripler
user=> (tripler 3)
9
The theme of providing useful functions for common actions is a major feature of Clojure. It provides many functions simply to make its programming more concise, expressive, and joyful.
Common Data Structures
Clojure has the commonly used data structures you should expect from a modern programming language.
As a functional language Clojure has immutable data structures. If you aren’t familiar with their use the quick gist is that whenever you manipulate data such as adding a new item to a list then you don’t mutate that list: a new list is created with the additional item.
Vectors
An ordered collection of items in a continuous block of memory (i.e. an array) that are defined with brackets.
[1 2 3 "four" ["any data even another vector"]]
(def v [1 2 3 4])
(count v) ; 4
(first v) ; 1
(rest v) ; (2 3 4)
Lists
Lists are also an ordered collection of items, but rather than being stored in a continuous block of memory they are implemented as linked lists. That has ramifications for when it’s appropriate to use either data structure: e.g. it’s trivial to add a new item to the beginning of a list but relatively expensive to add a new item to the beginning of a vector.
In practice vectors are far more commonly used.
Lists are defined with parentheses which means they need a slight tweak over what we’d expect so Clojure doesn’t confuse them with expressions: we need to prepend them with '
'(1 2 3 "four" ("a nested list"))
(def l '(1 2 3 4))
(count l) ; 4
(first l) ; 1
(rest l) ; (2 3 4)
Commas?
You may have noticed above that neither vectors nor lists used commas between the values. Clojure neatly does away with any comma arguments by treating them as whitespace. You can use them if you must but the common approach is to simply omit them.
;; these vectors are all the same data
(= [1 2 3 4] [1,2,3,4] [1,,,,,2,3,4] [1,2,3 4]) ; true
;; commas are whitespace even for expressions
(+,1,2,3,4) ; 10
(+,1,2,3,4,) ; 10
Maps
Data structures that associate keys and values (i.e. dictionaries or hashes). Note commas aren’t required between sets of key/values (because commas are whitespace).
Any value can be a key but Clojure programs usually use keywords as keys e.g. :keyword
. Keywords can be used as functions themselves to retrieve values from a map.
;; a bare map
{:key "value" :another-key "another value"}
;; assigned to a variable
(def ps5 {:name "Playstation 5"
:publisher "Sony"
:released 2020})
(:publisher ps5) ; "Sony"
(keys ps5) ; (:name :publisher :released)
(conj ps5 {:output :hdmi})
;; {:name "Playstation 5", :publisher "Sony", :released 2020, :output :hdmi}
ps5
;; {:name "Playstation 5", :publisher "Sony", :released 2020}
Note that the conj
(conjoin) function above does not modify the ps5 variable. It returns a new map with the additional data.
Documentation
Clojure has inline documentation you can access in its repl.
user=> (doc conj)
-------------------------
clojure.core/conj
([coll x] [coll x & xs])
conj[oin]. Returns a new collection with the xs
'added'. (conj nil item) returns (item). The 'addition' may
happen at different 'places' depending on the concrete type.
user=> (doc +)
-------------------------
clojure.core/+
([] [x] [x y] [x y & more])
Returns the sum of nums. (+) returns 0. Does not auto-promote
longs, will throw on overflow. See also: +'
user=> (doc doc)
-------------------------
clojure.repl/doc
([name])
Macro
Prints documentation for a var or special form given its name,
or for a spec if given a keyword
Wrapping Up
You’ve now seen some Clojure! This isn’t even close to a comprehensive overview of this powerful and expressive language. Clojure has many more constructs to help write powerful programs simply.
If you’re interesting in learning more I highly recommend Russ Olsen’s Getting Clojure from the Pragmatic Programmers.