Home Archives Search Feed

See Some Clojure

Published January 19, 2021

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.

xkcd Lisp Cycles comicxkcd Lisp Cycles comic

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.

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.

Last modified February 2, 2021   #clojure     #programming  


← Newer post  •  Older post →