stats

Read Me Clojure, Pt. 3

Basic Constructs

Image

The specific syntax of Lisp dialects allows one to precisely define and distinguish basic constructs, add new syntactic elements, and transform program forms as data during macro expansion. This stems from the use of simple yet well-thought-out ways of organizing and representing source code.

Basic Constructs

Programs written in Lisp variants are characterized by simple syntactic rules. Rather than discussing them theoretically, we will begin with a practical example that we will refer back to in order to learn the basic mechanisms governing the translation of source code into a form understandable by a computer. By tracing what the compiler does, we will better understand the constructs of the language.

Here is our base example:

(print "Hello, Lisp!")
(print "Hello, Lisp!")

Not particularly difficult. Is it?

Syntax

Lisp looks like oatmeal with toenail clippings mixed in.

– Larry Wall

Let us start with syntax. The first thing that catches the eye when we see programs written in Lisp dialects is the placement of nearly every compound construct within parentheses. In other programming languages, parentheses serve to group selected syntactic elements, e.g., arguments when calling or defining a function, sets of conditions, or operations on values. In Lisps, parentheses are the fundamental lexical element used to give shape to the entire program and to every expression.

In Lisp-family languages we construct expressions using the so-called Polish notation (PN), also known as prefix notation. It consists of placing the operator (function name) first, followed by the operands (call arguments). Parentheses are used to mark the beginnings and ends of expressions.

Prefix notation differs from the infix notation popular in many programming languages, but not enough to make it very difficult to recognize the individual parts of expressions. Our sample program can be expressed in Ruby as follows:

print "Hello, Lisp!"
print "Hello, Lisp!"

And in C in the following way:

#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("Hello, Lisp!");
    return 0;
}
#include &lt;stdio.h&gt; int main(int argc, char *argv[]) { printf(&#34;Hello, Lisp!&#34;); return 0; }

The differences between the discussed notations can be well illustrated with mathematical operations. Let us look at two simple computations:

2 + 2 * 3
2 + 2 * 3

And the notation conforming to Clojure syntax:

(+ 2 (* 2 3))
(+ 2 (* 2 3))

We can notice that in the second example the addition operator and its operands are enclosed in parentheses, and the operation appears in the first position of each evaluated list. An advantage of this notation is that there is no need to remember operator precedence.

Furthermore, prefix notation gives nested invocations one regular shape. Clojure still uses other delimiters for literal vectors, maps, and sets, but it does not need a separate precedence table for infix operators. The benefit here is grammatical regularity, not an inherent promise that every prefix parser will be faster.

Reader

Let us return to our program:

(print "Hello, Lisp!")
(print &#34;Hello, Lisp!&#34;)

The first step is reading text into memory. The reader consumes the next form from a character stream and returns the Clojure value represented by that form. Opening a file or terminal stream belongs to the caller; the reader itself is useful independently of compilation or evaluation. For explanation we can distinguish two conceptual tasks:

  1. detecting known lexical constructs in the text;

  2. extracting grammatically correct expressions from the detected constructs and representing them as internal, in-memory structures.

Lexical analysis

The first phase of reading program sources into memory is lexical analysis. It consists of:

  • cleaning the input of unnecessary symbols;

  • recognizing in the character stream sequences matching the lexical units defined in the language lexicon, in this context called tokens;

  • extracting from the text fragments that have syntactic significance (so-called lexemes), which are a kind of instances of the detected tokens.

Viewed lexically, our example contains the following significant fragments:

Lexeme Token name
( list literal (opening)
print symbol
"Hello, Lisp!" string literal
) list literal (closing)

This vocabulary is a useful descriptive model; it does not imply that Clojure exposes a separate token stream or a public tokenizer. The public operation, read, consumes one complete form and returns its value.

Syntactic analysis

The second conceptual task is parsing. It encompasses:

  • recognizing syntactic constructs in the stream of lexemes by comparing their kinds and positions against the grammatical rules of the language;

  • reading nested forms according to delimiter and macro-character rules;

  • producing in-memory representations of the found expressions using appropriate data structures;

The result is Clojure data representing the next form. The compiler may later analyze that data into its own internal representation, but the reader’s result is not that compiler AST.

Reader forms

Reader forms are textual notations that the Clojure reader recognizes and turns into values.

The table below lists the basic reader forms. The first column contains the token name, the second column shows example lexemes, and the last column indicates the data type of the value returned by the reader, provided the input is valid.

Token name Example lexemes Data type
symbol one
space/two
Symbol
nil literal nil nil
keyword literal :one
::two
:space/x
::space/y
Keyword
string literal "one two" java.lang.String
list literal (1 2 3) PersistentList
vector literal [1 2 3] PersistentVector
map literal {:a 1 :b 2}
#::{:a 1 :b 2}
#:space{:a 1 :b 2}
PersistentArrayMap
PersistentHashMap
boolean literal true, false java.lang.Boolean
integer literal 1
0xff
017
2r1101
java.lang.Long
rational literal 1/2 Ratio
big number literal 1.2M
1N
java.math.BigDecimal
Bigint
float literal -2.7e-4 java.lang.Double

Reader macros

Some characters trigger special reading behavior and are traditionally called reader macros or macro characters. Clojure’s behavior combines built-in rules with an internal read table, but programs cannot modify that table. Namespaced tagged literals provide a controlled extension point by mapping a tag to a data-reader function.

The following table presents reader macros:

Token name Example lexemes Reader result
quoting (quote) 'one
'(one two)
list beginning with quote
syntax quoting
(syntax-quote)
`one
`(two three)
varies
syntax
unquoting

(syntax unquote)
~quote varies
syntax unquoting
with splicing

(syntax unquote-splicing)
~@(list 1 2) varies
metadata map
(metadata map)
^{:doc "Description"} x next form, with metadata
metadata key
(metadata key)
^:dynamic x next form, with metadata
metadata tag
(metadata tag)
^Integer x next form, with metadata
comment
(comment)
; comment none
character literal
(character literal)
\a, \b, \c, \newline java.lang.Character
dereference expression
(dereference expression)
@x list beginning with deref
dispatch macro (dispatch macro) # varies

The last entry in the table is the so-called dispatch macro. This is a general term for a subgroup of reader macros whose tokens all begin with the hash symbol (#). When the reader encounters this character, it passes control over further analysis of the construct to a separate macro table. Below is a list of tokens handled using the dispatch macro:

Token name Example lexemes Reader result
Var quoting
(var-quote)
#'x list beginning with var
set literal
(set literal)
#{1 2 3} PersistentHashSet
regular expression
(regular expression)
#"one.tw[oO]" java.util.regex.Pattern
anonymous function literal
(anonymous function literal)
#(pr %)
#(pr %1 %2)
list beginning with fn*
ignore next form
(ignore next form)
#_ one two none
record/type literal
(record/type literal)
#name.type[:x]
#name.record{:x 1}
instance of the named Clojure type
reader conditional
(reader conditional)
#?(:clj "Clojure"
:cljs "ClojureScript")
selected form or none
tagged literal
(tagged literal)
#symbol argument varies
inst tagged literal
(inst tagged literal)
#inst "2018-11-12" java.util.Date
UUID tagged literal
(UUID tagged literal)
#uuid "88b05082-392d-4e0a-89c9-cf62ec375c43" java.util.UUID

Several entries expand to ordinary lists that only acquire executable meaning later. For example, @x reads as (clojure.core/deref x), and #'x reads as (var x). Conversely, #_ causes the following form to be skipped completely. Tagged literals invoke registered data-reader functions during reading, so reading trusted Clojure source and parsing untrusted EDN are distinct security contracts.

S-expressions

At the grammatical level, every element of source code in Lisp is a symbolically written expression. The textual representations of expressions – those we see in an editor – are called symbolic expressions, abbreviated as S-expressions (sexprs, sexps).

The structure of S-expressions somewhat resembles XML or JSON – that is, we are dealing with a notation that allows expressing nested and ordered sets of values, though in a somewhat simpler way than the aforementioned formats.

An S-expression in Lisp can be defined as a kind of notation in which:

  • every element is an expression:
    • non-compound (called an atom) or
    • composed of S-expressions enclosed in parentheses and separated by a separator.
S-expression examples in Lisp
print                     ; atomic S-expression (not a list of S-expressions)
"Hello, Lisp!"            ; atomic S-expression (not a list of S-expressions)
(print "Hello, Lisp!")    ; list S-expression
()                        ; both list and atomic S-expression
(print (+ 2 2))           ; S-expression composed of nested S-expressions
print ; atomic S-expression (not a list of S-expressions) &#34;Hello, Lisp!&#34; ; atomic S-expression (not a list of S-expressions) (print &#34;Hello, Lisp!&#34;) ; list S-expression () ; both list and atomic S-expression (print (+ 2 2)) ; S-expression composed of nested S-expressions

An S-expression in Clojure will be defined in a somewhat richer way, because we have additional collection literals there. It will be a kind of notation in which:

  • every element is an expression:
    • non-compound (called an atom) or
    • composed of separated S-expressions:
      • in pairs, enclosed in curly braces – {...};
      • individually, enclosed in:
        • round parentheses – (...);
        • curly braces with a hash symbol – #{...};
        • square brackets – [...].

In the case of expressions in curly braces, the first elements of pairs must be unique values within the entire expression, and in the case of expressions in curly braces preceded by a hash symbol, each value must be unique. This check is performed already during syntactic analysis, and when a given element requires prior computation, during evaluation.

S-expression examples in Clojure
:one                      ; atomic S-expression
1                         ; atomic S-expression
print                     ; atomic S-expression
"Hello, Lisp!"            ; atomic S-expression
(print "Hello, Lisp!")    ; list S-expression
[1 2 3]                   ; vector S-expression
#{1 2 3}                  ; set S-expression
{:one 1 :two 2 :three 3} ; map S-expression
(str [1 2 3])             ; list and vector S-expressions
:one ; atomic S-expression 1 ; atomic S-expression print ; atomic S-expression &#34;Hello, Lisp!&#34; ; atomic S-expression (print &#34;Hello, Lisp!&#34;) ; list S-expression [1 2 3] ; vector S-expression #{1 2 3} ; set S-expression {:one 1 :two 2 :three 3} ; map S-expression (str [1 2 3]) ; list and vector S-expressions

The recursive definition of an S-expression may seem difficult to understand, so we will aid ourselves with our one-line program and perform a manual categorization of the elements present in it.

In the notation (print "Hello, Lisp!"):

  • (...) is an S-expression, because it is a symbolically written list of S-expressions;
  • print is an S-expression, because it is an atom;
  • "Hello, Lisp!" is an S-expression, because it is an atom.

Graphically, this set can be presented as follows:

S-expression

Unlike other Lisps, compound S-expressions in Clojure are built not only from lists marked by round parentheses, but also using additional markers that correspond to certain kinds of collections. Depending on the notation used, a data structure representing the appropriate kind of S-expression will be created in memory:

Notation Literal S-expression Structure Data type
(a...z) list list list PersistentList
[a...z] vector vector vector PersistentVector
{a b ... x y} map map map PersistentArrayMap
PersistentHashMap
#{a...z} set set set PersistentHashSet

Atoms

When discussing symbolic expressions, we mentioned a specific class of them called atoms. An atom is an element of Lisp syntax that is not compound (is not: a list, a set, a vector, or a map). The exceptions are the empty list, the empty set, the empty vector, and the empty map, which are both compound expressions and atoms.

However, one more important condition must be added to the above definition, a condition that determines whether a symbolic notation can be considered a Lisp atom. Let us recall our program:

(print "Hello, Lisp!")
(print &#34;Hello, Lisp!&#34;)

In the previous examples, we could see that print and Hello, Lisp! are atoms, but would any arbitrary set of characters that is not a pair of parentheses with content qualify? No. An atom must be a valid syntactic construct on the basis of which the reader can decide what value to return. There is a certain rigor here. In this particular case, the text print will be stored as a symbol, and Hello, Lisp! as a string, because they satisfy the syntactic requirements for representing specific data structures.

Admittedly, the syntactic rules are liberal enough that most randomly typed words or even single characters will be recognized as symbols (and thus atoms), but placing a parenthesis in a symbolic label or starting it with a digit would be serious violations, and the reader would stop cooperating with us.

It is worth noting that the concept of an atom as a class of syntactic expressions is not widely used among Clojure programmers. This is probably because in Clojure there exists a reference data type called Atom which helps perform concurrent data operations.

List S-expressions

The most commonly encountered class of S-expressions are list S-expressions. It is thanks to them that programs written in Lisp dialects consist of a large number of parentheses.

A list S-expression in Clojure should be a list of elements (other S-expressions) separated by spaces, commas, or both. The beginning and end of a list S-expression should be marked by an opening and closing round parenthesis.

When a non-empty list is evaluated, its first element determines how the form is handled. A symbol in that position may name a special form, a macro, or a Var whose value is callable; any other operator expression is evaluated and must likewise yield an IFn. For an ordinary function call, the remaining elements are evaluated from left to right and passed as arguments.

If no elements are provided, a list S-expression will produce an empty list.

List S-expression examples
 1;; calling the + function
 2
 3(+ 1 2 3)
 4;=> 6
 5
 6;; calling the defn macro
 7;; used to define a named function
 8
 9(defn greet [] "Hello!")
10;=> #'user/greet
11
12;; calling the defined greet function
13
14(greet)
15;=> "Hello!"
16
17;; special form def
18;; used to define a global variable
19
20(def x 2)
21;=> #'user/x
22
23;; doubling the value of the global variable
24;; identified by the symbol x
25
26(+ x x)
27;=> 4
28
29;; empty list
30
31()
32;=> ()
;; calling the + function (+ 1 2 3) ;=&gt; 6 ;; calling the defn macro ;; used to define a named function (defn greet [] &#34;Hello!&#34;) ;=&gt; #&#39;user/greet ;; calling the defined greet function (greet) ;=&gt; &#34;Hello!&#34; ;; special form def ;; used to define a global variable (def x 2) ;=&gt; #&#39;user/x ;; doubling the value of the global variable ;; identified by the symbol x (+ x x) ;=&gt; 4 ;; empty list () ;=&gt; ()

Older Lisp dialects supported a somewhat different form of list S-expressions. Inside a pair of parentheses one did not place a list of all elements, but only one cell of the list, divided into a left and right value separated by a dot, with the end of the list marked by the symbol nil, e.g.:

(+ . (1 . (2 . nil)))
(+ . (1 . (2 . nil)))

In Clojure, this kind of notation is not supported, although there are data structures that allow performing operations on individual cells and creating so-called sequences.

Vector S-expressions

Vector literals create so-called vector S-expressions, but unlike list expressions, elements placed in their first positions have no special significance. The result of using a vector S-expression in its basic form will be a data structure called a vector, and each of its elements will become a component of it after its value has been computed.

Using vector S-expressions we can:

  • create the aforementioned vectors and use them for application logic,

  • specify argument lists of defined functions and macros,

  • express bindings of symbols to values in appropriate special constructs (including let and binding),

  • perform so-called positional destructuring of compound structures with a sequential access interface.

Vector S-expression examples
;; literal vector

[1 2 3 4]
;=> [1 2 3 4]

;; literal vector

[(+ 1 1) 2 3 4]
;=> [1 2 3 4]

;; bindings vector in a let form
;; creates a lexical binding of symbol a to value 1

(let [a 1] a)
;=> 1

;; vector binding form in a let form (destructuring)
;; inside the bindings vector of the let form
;; binds each symbol from the S-expression [a b]
;; to an initializing value from the S-expression [1 2]
;; according to position

(let [[a b] [1 2]]
  (+ a b))
;=> 3

;; argument list in a named function definition

(defn add
  [a b]
  (+ a b))
(add 2 2)
;=> 4

;; empty vector

[]
;=> []
;; literal vector [1 2 3 4] ;=&gt; [1 2 3 4] ;; literal vector [(+ 1 1) 2 3 4] ;=&gt; [1 2 3 4] ;; bindings vector in a let form ;; creates a lexical binding of symbol a to value 1 (let [a 1] a) ;=&gt; 1 ;; vector binding form in a let form (destructuring) ;; inside the bindings vector of the let form ;; binds each symbol from the S-expression [a b] ;; to an initializing value from the S-expression [1 2] ;; according to position (let [[a b] [1 2]] (+ a b)) ;=&gt; 3 ;; argument list in a named function definition (defn add [a b] (+ a b)) (add 2 2) ;=&gt; 4 ;; empty vector [] ;=&gt; []

Map S-expressions

Using the map literal we can construct map S-expressions. In their basic form they allow expressing an associative data structure called a map, which consists of indexed key–value pairs. Values should be separated by spaces, commas, or both. The first element of each pair is called the key, and the second the value.

Using map S-expressions we can:

  • create the aforementioned maps and use them for application logic,

  • specify named argument lists of defined functions and macros,

  • construct so-called metadata maps that allow enriching certain constructs with metadata that can describe them or control their properties;

  • perform associative destructuring of compound structures with an associative access interface.

Map S-expression examples
;; literal map

{"a" 1 "b" 2}
;=> {"a" 1 "b" 2}

;; map expressing named arguments
;; of a defined function
;; keys a and b must be strings

(defn add [& {:strs [a b]}]
  (+ a b))

;; calling a function with named arguments

(add "b" 1, "a" 3)
;=> 4

;; metadata map of a defined function
;; with a documentation string (key :doc)

(defn add
  {:doc "This function adds two numbers."}
  [a b]
  (+ a b))
;=> #'user/add

;; calling the documentation string for add
(doc add)
;=>> -------------------------
;=>> user/add
;=>> ([a b])
;=>>   This function adds two numbers.
;=> nil

;; map binding form in a let form (destructuring)
;; inside the bindings vector of the let form
;; binds symbols a and b
;; to values from a map indexed by "a" and "b"

(let [{:strs [a b]} {"b" 1 "a" 3}]
  (+ a b))
;=> 4

;; empty map

{}
;=> {}
;; literal map {&#34;a&#34; 1 &#34;b&#34; 2} ;=&gt; {&#34;a&#34; 1 &#34;b&#34; 2} ;; map expressing named arguments ;; of a defined function ;; keys a and b must be strings (defn add [&amp; {:strs [a b]}] (+ a b)) ;; calling a function with named arguments (add &#34;b&#34; 1, &#34;a&#34; 3) ;=&gt; 4 ;; metadata map of a defined function ;; with a documentation string (key :doc) (defn add {:doc &#34;This function adds two numbers.&#34;} [a b] (+ a b)) ;=&gt; #&#39;user/add ;; calling the documentation string for add (doc add) ;=&gt;&gt; ------------------------- ;=&gt;&gt; user/add ;=&gt;&gt; ([a b]) ;=&gt;&gt; This function adds two numbers. ;=&gt; nil ;; map binding form in a let form (destructuring) ;; inside the bindings vector of the let form ;; binds symbols a and b ;; to values from a map indexed by &#34;a&#34; and &#34;b&#34; (let [{:strs [a b]} {&#34;b&#34; 1 &#34;a&#34; 3}] (+ a b)) ;=&gt; 4 ;; empty map {} ;=&gt; {}

Set S-expressions

The set literal enables writing set S-expressions. Thanks to them, one can express sets in a clear and straightforward way – that is, structures in which each element appears only once.

The set literal consists of curly braces preceded by a hash symbol, inside which unique-within-the-set values are placed. Elements of a set S-expression should be separated by a space, a comma, or both.

If an element of a set expression is not a constant value, it will be computed before the object representing the set is created.

Set S-expression examples
#{1 2 3 4}
;=> #{1 2 3 4}

#{1 (+ 1 1) 3 4}
;=> #{1 2 3 4}

;; empty set

#{}
;=> #{}
#{1 2 3 4} ;=&gt; #{1 2 3 4} #{1 (+ 1 1) 3 4} ;=&gt; #{1 2 3 4} ;; empty set #{} ;=&gt; #{}

Reader-produced forms

Let us see what the reader returns for our program:

(print "Hello, Lisp!")
(print &#34;Hello, Lisp!&#34;)

The opening parenthesis introduces a list. The reader places a Symbol value named print in its first position and the Java string "Hello, Lisp!" in its second position. The result is therefore ordinary Clojure data: a list containing a symbol and a string.

Converting text into a Clojure list

This reader-produced list should not be confused with the compiler’s internal abstract syntax tree. The compiler analyzes the list in a lexical and namespace context and may represent the resolved invocation using private implementation data. That analyzed representation is not what read returns and is not the value handed to a macro.

Homoiconicity

Clojure is called homoiconic because programs are represented by the same kinds of immutable data structures that programs routinely manipulate. The correspondence is structural rather than textual: comments, whitespace, commas, and many reader-macro spellings are not preserved. For example, 'x and (quote x) read to equivalent data even though their character sequences differ.

This property lets syntactic macros accept unevaluated forms as data and return replacement forms for further compiler analysis. It does not require exposing the compiler’s AST or modifying already executing machine code.

Semantics

The next layer is evaluation: assigning meaning to a reader-produced form in a particular lexical and namespace context and obtaining its value. JVM Clojure has no separate interpreter for these forms. The compiler analyzes them, expands macros where needed, emits bytecode, and executes that bytecode; the REPL merely makes this cycle feel immediate.

Let us recall our example once more:

(print "Hello, Lisp!")
(print &#34;Hello, Lisp!&#34;)

After reading, the compiler receives one list containing a symbol and a string. It analyzes the list as a single form to obtain the value of the outermost expression.

In the given example, the first element of the list is the symbol print, which identifies a built-in Clojure function used to display text on the screen. By examining lexical bindings and the mappings maintained by the current namespace, the compiler resolves the symbol to the Var clojure.core/print and recognizes a function call. At execution time, the callable value of that Var is invoked.

The remaining element is the function argument. Ordinary function-call operands are evaluated from left to right. A string evaluates to itself, so the resulting java.lang.String value is passed to print.

Running the print function subroutine will produce the intended side effect of displaying the following on screen:

Hello, Lisp!

Additionally, the function will return a value that, depending on how the program was launched, will be displayed (in the case of an interactive console) or remain unhandled (in other cases).

Forms

A form is a Clojure value presented to the compiler for evaluation. Lists are important forms, but forms are not all nested lists: symbols, literals, vectors, maps, and sets can also be evaluated.

Meaning depends on both value and context. For example, the symbol x in (x 1) may resolve to a function, a map, a vector, a set, a keyword, or another object implementing clojure.lang.IFn. The list is then an ordinary call, and the callable’s own contract determines what the argument 1 means.

Before we proceed to discussing the main kinds of forms, let us name those that will be recognized in our program:

Forms in the example program
(print "Hello, Lisp!")
(print &#34;Hello, Lisp!&#34;)
  • (...) – list form,
  • print – symbol form,
  • "Hello, Lisp!" – self-evaluating form,
  • (print "Hello, Lisp!") – function-call form:
    • after resolving print to a Var whose value is callable;
    • after evaluating the string argument.

Self-evaluating forms

Strings, numbers, characters, keywords, true, false, and nil evaluate to themselves. Other objects not assigned special evaluation rules generally do so as well. Empty collections are also values; non-empty vector, map, and set forms first evaluate their elements.

Self-evaluating form examples
"Hello, Lisp!"    ; string
\a                ; character
()                ; empty list
123               ; integer
:lalala           ; keyword
&#34;Hello, Lisp!&#34; ; string \a ; character () ; empty list 123 ; integer :lalala ; keyword

The quote special form follows a different rule: it returns its argument as data without evaluating that argument. The quoting mechanism will be discussed shortly.

Symbol forms

A symbol will be treated by the evaluator as a symbol form, and it will attempt to find the object bound to it, which – depending on the context – may be assigned to that symbol’s name in various places, e.g., in lexical bindings or namespaces (and global variables). Resolving a symbol obtains the associated value; it does not rewrite the reader-produced data structure in place.

Symbol form examples
print
+
print +

See also:

Compound forms

Vectors, maps, and sets are collection forms. Their element expressions are evaluated and the resulting values are collected into a vector, map, or set. A list follows the operator rules described below instead of evaluating merely as a collection.

Examples of common collection forms
 [1 2 3]        ; vector form
#{1 2 3}        ; set form
 {"a" 1 "b" 2}  ; map form
'(1 2 3)        ; list form (literal list)
[1 2 3] ; vector form #{1 2 3} ; set form {&#34;a&#34; 1 &#34;b&#34; 2} ; map form &#39;(1 2 3) ; list form (literal list)

For a non-empty list or sequence, the compiler first checks whether the operator names a special form or macro. Otherwise the operator and operands are evaluated from left to right, and the operator’s value is invoked as an IFn.

Lookup forms

Vectors, sets, and maps implement IFn, so they may occupy operator position. A vector looks up an index, a set looks up a member, and a map looks up a key. Map calls also accept a second, not-found value; accepted arities are part of each type’s callable contract.

Collection lookup form examples
([1 2 3]         0)  ;=> 1
(#{1 2 3}        2)  ;=> 2
({"a" 1 "b" 2} "b")  ;=> 2
([1 2 3] 0) ;=&gt; 1 (#{1 2 3} 2) ;=&gt; 2 ({&#34;a&#34; 1 &#34;b&#34; 2} &#34;b&#34;) ;=&gt; 2

Keywords and literal symbols also implement IFn and can look themselves up in an associative collection. They accept either the collection alone or the collection and a not-found value.

Keyword and symbol lookup form examples
(:a {:a 1 :b 2})       ;=> 1
(:a #{:a :b :c})       ;=> :a
('a {'a 1 'b 2 :c 3})  ;=> 1
(:a {:a 1 :b 2}) ;=&gt; 1 (:a #{:a :b :c}) ;=&gt; :a (&#39;a {&#39;a 1 &#39;b 2 :c 3}) ;=&gt; 1
Function-call forms

If the evaluated operator is a function object, the computed operand values are passed to its invoke method. Java interop uses compiler-recognized host-expression forms; Java methods themselves are not Clojure function objects.

Function-call form examples
(print "Hello, Lisp!")  ;=> nil =>> Hello, Lisp!
(+ 2 2)                 ;=> 4
(.toLowerCase "A")      ;=> a
(print &#34;Hello, Lisp!&#34;) ;=&gt; nil =&gt;&gt; Hello, Lisp! (+ 2 2) ;=&gt; 4 (.toLowerCase &#34;A&#34;) ;=&gt; a
Special forms

If the first symbol names a special form, the compiler applies that form’s own grammar and evaluation rules. Special forms are recognized directly by the compiler; they are not Vars containing functions.

Special form usage examples
(def   x 1)                           ; defining a global variable
(fn    [x] (inc x))                   ; creating a function
(def   f (fn [x] (inc x)))           ; defining a named function
(let   [a 1] a)                       ; creating lexical bindings
(quote (1 2 3))                       ; quoting
(. System (getProperty "user.home"))  ; accessing Java classes
(def x 1) ; defining a global variable (fn [x] (inc x)) ; creating a function (def f (fn [x] (inc x))) ; defining a named function (let [a 1] a) ; creating lexical bindings (quote (1 2 3)) ; quoting (. System (getProperty &#34;user.home&#34;)) ; accessing Java classes

Special forms are – as the name indicates – forms with rules that differ from ordinary function calls. Some introduce bindings, some control evaluation, and some interact with the host platform. Their operands are processed exactly as each special form specifies.

Thanks to special forms we can, for example, define global variables or functions, and also create bindings of symbols to values in certain areas of a program.

Nested evaluation

Evaluation follows the rule for the form at hand. Ordinary calls evaluate operator and operands; collection forms evaluate their elements; special forms and macros specify their own behavior. A value returned by one of these operations is a result, not automatically treated as another form to evaluate. In particular, resolving a symbol bound to a list returns that list as a value rather than executing it.

Invalid forms

If a form violates the relevant rule – for example, if its operator does not implement IFn, its symbol cannot be resolved, or its operands have incompatible types – evaluation throws an exception. Whether the surrounding process terminates depends on its error handling; a REPL normally reports the error and continues.

Examples of S-expressions that are not forms
1(1 2 3)       ; the number 1 is not callable
2(())          ; the empty list is not callable
3(+ /)         ; + cannot add the function value held by /
4(no_such)     ; no construct identified by the symbol no_such
5no_such       ; no construct identified by the symbol no_such
(1 2 3) ; the number 1 is not callable (()) ; the empty list is not callable (+ /) ; + cannot add the function value held by / (no_such) ; no construct identified by the symbol no_such no_such ; no construct identified by the symbol no_such

The names of some forms are conventional – that is, they are based on the assumption that certain S-expressions can be computed to data of specific types. For example, the notation:

(add 1 2 3)
(add 1 2 3)

we will conventionally call a function-call form, even though in reality we are not certain whether the symbol form add will be recognized in the namespace as a global variable, and whether the reference object found there will contain a reference to a function.

Binding forms

A binding form is a construct in which a value is assigned to a symbolic identifier in order to create a binding. At the syntactic level, these forms will contain unquoted symbols, but they will not be treated as symbol forms whose values need to be resolved; rather, they serve as expressions that assign in-memory objects to symbolic names.

We will recognize a binding form in expressions representing certain special forms, where we will encounter:

  • a pair composed of an unquoted symbol and another form (representing the value that, once computed, is to be assigned);

  • an unquoted symbol appearing on its own, e.g., in a function’s parameter vector, where invocation will establish a lexical binding to the corresponding argument value.

Symbol binding forms are components of certain compound forms and special forms. We will find them, for example:

Additionally, binding forms may also appear in so-called structural bindings – that is, abstract bindings in which compound data structures are destructured and symbols are assigned values at specified positions or identified by given keys. We will then encounter a vector binding form or a map binding form, in which instead of a single unquoted symbol on the left side, a vector or map S-expression appears.

Some other constructs that use binding forms can also be intuitively referred to as binding forms. These are expressions whose primary purpose is to create bindings. For example, the special form let will also be called a let binding form, even though in reality it uses potentially many basic binding forms grouped in a bindings vector.

Binding form examples
(def x 5)       ; symbol x in a binding form
(fn [x] nil)    ; symbol x in a binding form
(let [x 2])     ; symbol x in a binding form

(defn some-fn   ; symbol some-fn in a binding form (function name)
  [x]           ; symbol x in a binding form (parameter name)
  nil)
(def x 5) ; symbol x in a binding form (fn [x] nil) ; symbol x in a binding form (let [x 2]) ; symbol x in a binding form (defn some-fn ; symbol some-fn in a binding form (function name) [x] ; symbol x in a binding form (parameter name) nil)

Function parameters establish lexical bindings when the function is invoked. They are distinct from dynamic Var bindings created with binding.

See also:

Quoting

An important special construct in Clojure and other Lisps is the special form quote, which returns its argument without evaluating that argument. The reader still constructs the complete argument value. During compiler analysis, quote instructs the compiler to return that value as data rather than resolve its symbols or invoke its lists.

In Lisp dialects, the quote form creates literal variants of structures that in their unquoted forms would be used to represent program source code and/or would be evaluated.

Examples of using the quote form
1(quote one)        ; literal symbol
2(quote (1 2 3))    ; literal list
3(quote [a b])      ; literal vector
4(quote {a 1 b 2})  ; literal map
5(quote #{1 2 3})   ; literal set
(quote one) ; literal symbol (quote (1 2 3)) ; literal list (quote [a b]) ; literal vector (quote {a 1 b 2}) ; literal map (quote #{1 2 3}) ; literal set

The above can also be written using syntactic sugar:

1'one
2'(1 2 3)
3'[a b]
4'{a 1 b 2}
5'#{a b}
&#39;one &#39;(1 2 3) &#39;[a b] &#39;{a 1 b 2} &#39;#{a b}

The same data structures could be produced without using quoting, by using the appropriate built-in functions of the language:

1(symbol "one")                            ; symbol
2(list 1 2 3)                              ; list
3(vector (symbol "a") (symbol "b"))        ; vector
4(hash-map (symbol "a") 1 (symbol "b") 2) ; map
5(hash-set 1 2 3)                          ; set
(symbol &#34;one&#34;) ; symbol (list 1 2 3) ; list (vector (symbol &#34;a&#34;) (symbol &#34;b&#34;)) ; vector (hash-map (symbol &#34;a&#34;) 1 (symbol &#34;b&#34;) 2) ; map (hash-set 1 2 3) ; set

The effect covers the complete quoted value: nested symbols and lists remain data as well.

Recursive quoting examples
1(quote (a b c))        ; list with literal symbols
2(quote (+ 2 (* 2 3)))  ; list with literal symbols and numbers
3(quote [one 2 3])      ; vector with a literal symbol and numbers
4(quote {a 1 b 2})      ; map with literal symbols and numbers
5(quote #{a b c})       ; set with literal symbols
(quote (a b c)) ; list with literal symbols (quote (+ 2 (* 2 3))) ; list with literal symbols and numbers (quote [one 2 3]) ; vector with a literal symbol and numbers (quote {a 1 b 2}) ; map with literal symbols and numbers (quote #{a b c}) ; set with literal symbols

The terms vector literal, map literal, and set literal describe reader syntax, not an evaluation guarantee for every nested form. When an unquoted collection form is evaluated, its elements are evaluated. Quoting the outer collection prevents that element evaluation because the whole argument of quote is returned as data.

Vector literals and quoting
[1 2 3]         ; a vector literal; its elements evaluate to themselves
[1 2 (inc 2)]   ; a vector literal evaluated to [1 2 3]
'[1 2 (inc 2)]  ; quote returns a vector containing the list (inc 2)
'[1 2]          ; quoting makes no observable difference here
[1 2 3] ; a vector literal; its elements evaluate to themselves [1 2 (inc 2)] ; a vector literal evaluated to [1 2 3] &#39;[1 2 (inc 2)] ; quote returns a vector containing the list (inc 2) &#39;[1 2] ; quoting makes no observable difference here

Let us also try quoting applied to our template program:

'(print "Hello, Lisp!")
&#39;(print &#34;Hello, Lisp!&#34;)

The result of evaluating the above expression is a value which is a list containing a symbol and a string – that is, our original program, expressible as the text:

(print "Hello, Lisp!")

Quoting a self-evaluating value such as a string produces the same value; quoting is most visible for symbols and lists, whose ordinary evaluation rules are different.

Identifiers

Identifiers are constructs that allow us to name identities (e.g., individual values or compound data structures placed in memory) so that we can later refer to them.

In Clojure there are identification constructs (symbols, discussed below) that carry special syntactic meaning and whose named objects are resolved automatically. There are also constructs (e.g., keywords) that serve to identify utility data, but their use is not mandatory.

Symbols

A symbol in Clojure is a data type used for names in forms and for symbolic data in applications. The reader merely constructs Symbol values; the compiler and binding constructs give those values contextual roles as local names, Var names, class names, or special-form identifiers.

In Clojure, symbol forms can be expressed in program text without any additional markers. When a symbol is evaluated as a form, it is resolved in the current lexical, namespace, and host context; it is not self-evaluating.

Symbol spelling admits letters, digits, and several punctuation characters, with special rules for initial digits, /, ., :, and the reserved symbol &. Because the exact reader grammar is richer than a short character whitelist, the official reader reference is the safer authority for unusual names.

Examples of symbol usage
1func                ; func is a symbol form
2(func 1 2 3)        ; func is a symbol form in a function-call form
3(fn [x y] x)        ; symbols x and y are parameters of an anonymous function
4                    ; [x y] contains two symbol binding forms
5(.toLowerCase "A")  ; toLowerCase is a Java class method
6
7; ChunkedSeq is an inner class of PersistentVector
8(new clojure.lang.PersistentVector$ChunkedSeq [1 2 3 4 5] 0 3)
func ; func is a symbol form (func 1 2 3) ; func is a symbol form in a function-call form (fn [x y] x) ; symbols x and y are parameters of an anonymous function ; [x y] contains two symbol binding forms (.toLowerCase &#34;A&#34;) ; toLowerCase is a Java class method ; ChunkedSeq is an inner class of PersistentVector (new clojure.lang.PersistentVector$ChunkedSeq [1 2 3 4 5] 0 3)

In many Lisps a symbol is a reference type, meaning it independently identifies another object by storing a reference to its in-memory structure. In Clojure things are different – a symbol does not hold any reference, and the fact that we can use symbol forms to invoke functions or refer to constant values is owed to the appropriate treatment by the evaluator and the searching of additional structures (e.g., namespaces or lexical bindings areas).

Dots may occur in namespace names and qualified Java class names. A dot at the beginning or end of a symbol is reserved for Clojure’s Java interop syntax.

Namespaced symbols

A symbol may optionally contain an additional name specifying a so-called namespace – a special collection that serves to group identifiers in order to eliminate conflicts. More details about using this visibility-control mechanism can be found later in this episode.

The namespace name is a string subject to the same syntactic rules as the symbol name, and is expressed by placing it before the symbol name and separating it with a slash (/), for example:

1my/func        ; symbol func with namespace my
2(my/func 1 2)  ; symbol func with namespace my in a function-call form
my/func ; symbol func with namespace my (my/func 1 2) ; symbol func with namespace my in a function-call form

In the symbol object itself we will find no reference to the namespace other than a textual one. Here too, no reference is stored. We can specify a nonexistent namespace and it will not be an error – not until the evaluator begins evaluating the notation.

Literal symbols

We encounter symbols in reader-produced forms, where compiler analysis may treat them as identifiers. We can also use quoted symbols as ordinary values in application logic. In that case they will serve, for example, as simple enumerated types, representing constant values belonging to fixed sets:

1(list (symbol "little") (symbol "much") (symbol "most"))
2(list 'little 'much 'most)
3'(little much most)
(list (symbol &#34;little&#34;) (symbol &#34;much&#34;) (symbol &#34;most&#34;)) (list &#39;little &#39;much &#39;most) &#39;(little much most)

The three notations above are equivalent. The first constructs a literal list whose elements are symbols created from strings using the built-in symbol function; the second also produces a list, but uses quoting to express literal symbols; the third makes use of recursive quoting of the entire list S-expression.

Quoted symbols may also include a namespace designation:

1(symbol "name" "space")
2'name/space
(symbol &#34;name&#34; &#34;space&#34;) &#39;name/space

In Lisps, literal symbols are sometimes used as indexing keys in associative structures (e.g., maps):

{ 'little 21, 'much 108, 'most 11 }
{ &#39;little 21, &#39;much 108, &#39;most 11 }

Symbols are not necessarily interned, but equal symbols have consistent equality and hashing semantics and are perfectly valid map keys. Keywords are more idiomatic for many application-data schemas because they evaluate to themselves and are conventionally used as associative keys, not because symbol lookup would be semantically unsafe.

See also:

Keywords

A keyword (colloquially called a key) is a data type that – similarly to symbols – serves to identify other objects, but in Clojure it carries no special syntactic meaning and keywords do not automatically identify other program constructs. Keywords are represented by objects of type clojure.lang.Keyword.

JVM Clojure interns keywords. Their public semantic contract, however, is value identity and equality; application code need not depend on a particular allocation strategy.

Keywords work well as simple enumerated types or as indices in associative data structures (e.g., maps). As for the built-in mechanisms of the Clojure language, we will encounter keywords in certain macros and in constructs that express named arguments bindings of functions.

From the syntactic point of view, every keyword has a name that must be a string beginning with a colon (:) and may contain alphanumeric characters as well as: *, +, !, -, _, and ?. In practice a slightly richer character set may be used (e.g., the dot is increasingly common for denoting internal hierarchies in some libraries), but this may change in the future, so it is worth using key names consistent with the language documentation.

Keywords are also functions. When we place a keyword in the first position of a list S-expression, a subprogram will be invoked that tries to look up an index in an associative or set collection given as an argument.

Like symbols, keywords may optionally include a namespace designation.

Examples of creating keywords
:a-key                              ; keyword
::a-key                             ; keyword with current namespace
:space/a-key                        ; keyword with namespace
::a-key                             ; keyword in the current namespace
(keyword "a-key")                   ; keyword from a string
(keyword "space" "a-key")           ; same but with namespace specified
{:a 1 :b 2}                         ; map with keyword indices
(defn x [& {:keys [color]}] color)  ; function with a named keyword argument
(x :color 123)                      ; calling function with keyword argument
(:a {:a 1 :b 2})                    ; keyword as a function searching a map
(:a #{:a :b :c :d})                 ; keyword as a function searching a set
:a-key ; keyword ::a-key ; keyword with current namespace :space/a-key ; keyword with namespace ::a-key ; keyword in the current namespace (keyword &#34;a-key&#34;) ; keyword from a string (keyword &#34;space&#34; &#34;a-key&#34;) ; same but with namespace specified {:a 1 :b 2} ; map with keyword indices (defn x [&amp; {:keys [color]}] color) ; function with a named keyword argument (x :color 123) ; calling function with keyword argument (:a {:a 1 :b 2}) ; keyword as a function searching a map (:a #{:a :b :c :d}) ; keyword as a function searching a set

See also:

Namespaces

In Clojure a construct called a namespace is used. In information technology this term denotes a mechanism for controlling the visibility of identifiers that allows their hierarchical grouping in order to avoid conflicts. Common examples of namespaces include file system directory structures, DNS records, and public IP addresses.

Namespaces in computer programming help separate sets of identifiers used in different components (e.g., programming libraries) or contexts, so that the exposed names (e.g., of modules, classes, variables, or functions) are unique. We can then use several libraries in a program that define functions with the same name. When referring to such a function, a so-called fully qualified name will be required – an identifier enriched with a namespace designation.

In Clojure, a namespace is a globally registered clojure.lang.Namespace object. It maintains mappings whose keys are symbols and whose values are Vars or imported Java classes. It can be inspected like a symbol table, but it is not an ordinary persistent map value.

Since symbols placed in a program may optionally include a namespace designation, it is possible to use them to refer to identifiers from different namespaces. If a symbol does not contain a fully qualified name, then during its evaluation it is assumed that it identifies a construct defined in the current namespace, pointed to by the dynamic global variable clojure.core/*ns*:

;; alias-qualified symbol select from namespace clojure.set
;; identifies a function

(require '[clojure.set :as set])
(set/select odd? #{1 2 3 4})
;=> #{1 3}

;; symbol + from namespace clojure.core
;; imported into the current namespace (user)
;; identifies a function

(+ 2 2)
;=> 4
;; alias-qualified symbol select from namespace clojure.set ;; identifies a function (require &#39;[clojure.set :as set]) (set/select odd? #{1 2 3 4}) ;=&gt; #{1 3} ;; symbol + from namespace clojure.core ;; imported into the current namespace (user) ;; identifies a function (+ 2 2) ;=&gt; 4

The first call uses the symbol set/select. Its namespace part is the local alias set, which compiler analysis resolves to clojure.set; the namespace mapping for select leads to a Var whose value is the function. The arguments odd? and #{1 2 3 4} are then evaluated and passed to that function.

The second symbol has no namespace part. In the user namespace, + resolves through a referred mapping to the Var clojure.core/+. Referring a Var preserves the same Var identity; it does not copy the function value into user.

See also:

Handling global state

When talking about Clojure, it is emphasized that there are no conventional variables and that data structures are immutable. Why, then, does the term “global variable” keep appearing?

Many applications must handle changing state, some of which is shared or globally reachable under stable names. Examples include: a character’s condition in a game, the arrangement of windows in a user interface, or the currently processed contents of a file being read.

To represent such data one might use conventional variables – fixed memory locations with given names whose contents are modified by appropriate subprograms (e.g., updating a score, reacting to user actions on interface elements, or reading data from a database). However, in such a model we cannot use multiple threads without applying additional isolation mechanisms. The memory slot of a variable representing an important parameter may be reorganized by one thread while another is still performing a critical operation with it. This means more work for the programmer, who instead of focusing on the application’s business logic must remember to safeguard the program against itself by introducing semaphores, locks, and similar constructs.

Clojure separates immutable values from identities that may acquire new values over time. Lexical locals are immutable bindings, while Vars, Atoms, Refs, and Agents offer different reference types contracts for coordinated change. This is not a ban on mutation inside the JVM; it is a design in which the permitted places and update semantics are explicit.

Updating a reference changes its logical current value according to that reference type’s coordination rules. The new value is often a persistent collection sharing structure with the old one, but neither a distinct memory address nor retained history is part of the general reference contract.

The Var type

A Var is the named reference used by Clojure’s global environment. It may have a root binding, shared by threads that lack a thread-local binding. Vars marked ^:dynamic may be rebound with binding for the current thread and dynamically nested calls; ordinary Vars do not gain per-thread isolation merely by being Vars.

By convention, Var objects in Clojure are interned in namespaces – there is no commonly used special form or function that would allow creating them without binding them to some symbolic identifier. There is, however, the with-local-vars macro, which creates Var objects in lexical scope – they must then be identified by given symbols visible within the expression passed as an argument. We can read the values they point to using the deref function or the dereference literal (the at sign placed before the symbol):

(with-local-vars [a 5] @a)
;=> 5
(with-local-vars [a 5] @a) ;=&gt; 5

See also:

Global variables

Objects of type Var (more precisely clojure.lang.Var) are used together with symbols in Clojure to create global variables. Global variables serve to identify infrequently changing identities, e.g., configuration values or functions defined in the program.

It works as follows: a namespace maps a symbolic name to a particular Var. The Var, in turn, supplies its current thread binding when one exists, otherwise its root binding. The namespace mapping is therefore symbol → Var, not symbol → current root value.

Let us look at a diagram illustrating how a function object is obtained from its name in our example program:

Resolving a symbolic function name

Compiler analysis and execution perform three conceptually distinct steps to reach the invoked function:

  1. The symbol print resolves through the current namespace’s referred mapping to the Var clojure.core/print.

  2. Execution obtains the Var’s current binding – a thread binding when present, otherwise the root binding.

  3. That binding contains a function object, which is invoked with the evaluated arguments.

Creating global variables is possible using the def special form:

 1(def x 8)                ; x points to the value 8
 2(def y [1 2 3])          ; y points to the vector [1 2 3]
 3(def write print)        ; write points to the current value of print variable
 4(declare later)          ; creates a global variable without binding to a value
 5
 6(write x)                ;=> 8
 7(write y)                ;=> [1 2 3]
 8(write "Hello, Lisp!")   ;=> Hello, Lisp!
 9
10later
11;=> #object[clojure.lang.Var$Unbound 0x53245a5b "Unbound: #'user/later"]
(def x 8) ; x points to the value 8 (def y [1 2 3]) ; y points to the vector [1 2 3] (def write print) ; write points to the current value of print variable (declare later) ; creates a global variable without binding to a value (write x) ;=&gt; 8 (write y) ;=&gt; [1 2 3] (write &#34;Hello, Lisp!&#34;) ;=&gt; Hello, Lisp! later ;=&gt; #object[clojure.lang.Var$Unbound 0x53245a5b &#34;Unbound: #&#39;user/later&#34;]

See also:

Bindings

A binding is, in general terms, an association of an identifier with an identified object. Using this term helps us avoid misunderstandings related to the subtle but – from the programmer’s perspective – important differences in how objects are referred to by names.

In imperatively rooted languages we often use the concept of a “variable” and expect, by force of habit, that it will have some name. The term lets us point to a memory slot in which we will find a value. In Clojure this approach could be misleading, because we can create nameless variables (reference objects) as well as name values that are not variables at all.

In Clojure we encounter several kinds of bindings:

The last item requires explanation, because – as is easy to notice – it contains no mention of any symbol, and we have repeatedly stated that it is symbols that serve to identify other objects.

At this point it is worth recalling that in Clojure symbols do not independently store references to values – this function is performed by reference objects (e.g., of type Var in the case of global variables). This follows from the adopted model of managing mutable state.

Examples of bindings
;; global variable
;; initial value pointed to by x is 1

(def x 1)
;=> #'user/x

;; parameter vector of a function definition
;; the first two arguments become parameters a and b

(fn [a b] (+ a b))
;=> #<Fn@5af25442 user/eval14406[fn]>

;; lexical binding
;; symbol a identifies the value 1

(let [a 1]
  a)
;=> 1

;; positional decomposition in let
;; symbol a bound to value 1 by position
;; symbol b bound to value 2 by position

(let [[a b] '(1 2 8)]
  (+ a b))
;=> 3

;; associative decomposition in let
;; symbol a bound to value 1 by key :a
;; symbol b bound to value 2 by key :b

(let [{a :a b :b} {:a 1 :b 2 :c 8}]
  (+ a b))
;=> 3

;; associative decomposition in let (using :keys)
;; symbol a bound to value 1 by key :a
;; symbol b bound to value 2 by key :b

(let [{:keys [:a :b]} {:a 1 :b 2 :c 8}]
  (+ a b))
;=> 3

;; binding a Var reference object to a value

(with-local-vars [x 5] x)
;=> #'Var: --unnamed-->

;; binding an Atom reference object to a value

(atom 5)
;=> #<Atom@a1cb453 5>
;; global variable ;; initial value pointed to by x is 1 (def x 1) ;=&gt; #&#39;user/x ;; parameter vector of a function definition ;; the first two arguments become parameters a and b (fn [a b] (+ a b)) ;=&gt; #&lt;Fn@5af25442 user/eval14406[fn]&gt; ;; lexical binding ;; symbol a identifies the value 1 (let [a 1] a) ;=&gt; 1 ;; positional decomposition in let ;; symbol a bound to value 1 by position ;; symbol b bound to value 2 by position (let [[a b] &#39;(1 2 8)] (+ a b)) ;=&gt; 3 ;; associative decomposition in let ;; symbol a bound to value 1 by key :a ;; symbol b bound to value 2 by key :b (let [{a :a b :b} {:a 1 :b 2 :c 8}] (+ a b)) ;=&gt; 3 ;; associative decomposition in let (using :keys) ;; symbol a bound to value 1 by key :a ;; symbol b bound to value 2 by key :b (let [{:keys [:a :b]} {:a 1 :b 2 :c 8}] (+ a b)) ;=&gt; 3 ;; binding a Var reference object to a value (with-local-vars [x 5] x) ;=&gt; #&#39;Var: --unnamed--&gt; ;; binding an Atom reference object to a value (atom 5) ;=&gt; #&lt;Atom@a1cb453 5&gt;

See also:

Collections

A collection is a data structure that can store a certain number of elements.

Lists

In Lisp the most commonly used data structure is the list, which – as we have had the chance to observe – serves both for the syntactic organization of program code (list S-expressions) and for storing ordered collections of elements for the purposes of application logic. It is worth thinking of a list as a way of arranging data, not merely a symbolic notation with parentheses.

There are several kinds of lists. The most commonly used in Lisp dialects are so-called linked lists, and more specifically singly linked lists. They are characterized by the ability to flexibly link elements together and to quickly add new ones to their beginnings. This is why they serve their role well as in-memory representations of Lisp program structures.

Lists in early Lisps

Historically speaking, each node of a list (in Lisp called a cons cell) has two slots: the first points to the value of the current element, and the second to the next element of the list (the next cons cell). In reality these are simply two pointers that can refer to arbitrary values.

Access to the first slot of each element is called car (from Contents of the Address part of Register number), and to the second cdr (from Contents of the Decrement part of Register number). The etymology of these peculiar names goes back to the time when Lisp was implemented on the IBM 704 computer (the 1950s). That machine had a special instruction that divided a 36-bit machine word into 4 parts – car and cdr are the abbreviated labels of the first two, and they found their way into Lisp because the author used them to divide the contents of the internal structure representing a list cell. In jargon, the first slot of a cons cell is therefore referred to as CAR, and the second as CDR.

There is nothing preventing a list from containing another list within it:

A Lisp list

In code we can express the above structure as:

(one two (1 2))
(one two (1 2))

that is:

1(one   ; first element of the list
2 two   ; second element of the list
3 (     ; third element of the list -- a nested list
4  1    ; first element of the nested list
5  2))  ; second element of the nested list
(one ; first element of the list two ; second element of the list ( ; third element of the list -- a nested list 1 ; first element of the nested list 2)) ; second element of the nested list

And here is the notation using so-called full notation, which is not used in Clojure. Inside each pair of parentheses we find two slots separated by a dot:

(one . (two . ((1 . (2 . nil)) . nil)))
(one . (two . ((1 . (2 . nil)) . nil)))

The dots separate the CAR and CDR registers of the cells, and the nil elements denote the ends of lists. If the second slot (CDR) is to point to the next element of the list, in this notation that next element is placed after the dot and enclosed in parentheses.

The notation (one two (1 2)) is valid reader syntax and can be presented as a form, but evaluating it will fail unless the operator and operands satisfy the ordinary call rules. We can instead use quoting so that evaluation returns the list as data:

'(one two (1 2))
&#39;(one two (1 2))

In the earliest editions of Lisp, lists were constructed using the cons function (from construct). For example:

(cons 'one (cons 'two (cons (cons 1 (cons 2 nil)) nil)))
(cons &#39;one (cons &#39;two (cons (cons 1 (cons 2 nil)) nil)))

Which can also be presented as:

1(cons 'one
2      (cons 'two
3            (cons (cons 1
4                        (cons 2 nil))
5                  nil)))
(cons &#39;one (cons &#39;two (cons (cons 1 (cons 2 nil)) nil)))

Such list construction may exercise the mind, but it does not serve productive programming. At present some Lisp dialects have dropped support for full notation, although the cons function is still used – mainly for performing operations on existing lists rather than constructing them from scratch.

Lists in Clojure

In Clojure, lists are represented by the host system’s object data type called clojure.lang.PersistentList. Objects of this type can be found in the syntax tree, where they reflect list S-expressions, as well as in application data when the list function is used or a literal list is produced by quoting its symbolically expressed form.

Examples of creating lists in Clojure
1(list 1 2 3)     ; using the list function (arguments will be evaluated)
2(quote (1 2 3))  ; literal list (arguments will not be evaluated)
3'(1 2 3)         ; literal list (arguments will not be evaluated)
(list 1 2 3) ; using the list function (arguments will be evaluated) (quote (1 2 3)) ; literal list (arguments will not be evaluated) &#39;(1 2 3) ; literal list (arguments will not be evaluated)

Internally, PersistentList objects are doubly linked lists, although this property is hidden from the programmer. Moreover, in Clojure a list (like most data structures) is immutable – to introduce a change in its structure one never modifies the object representing it but instead creates a new one that differs from the previous one.

See also:

“Lists”, chapter IX.

Sequences and Cons objects

Apart from encapsulated lists, Clojure also provides objects of type Cons (more precisely clojure.lang.Cons). They are the closest counterpart of cons cells known from other Lisp dialects. More precisely, using cons we can construct so-called sequences – abstract collections characterized by a uniform access interface consisting of three basic operations:

  • reading the value stored in the cell,
  • reading the next cell linked to the current one,
  • attaching a new cell to an existing one.

We can place references to values of arbitrary types in Cons objects and link them into sequences using the cons function. It accepts two arguments: a value and an object also equipped with a sequential interface (these include, among others, lists, vectors, and maps). The returned value will be a Cons cell whose next cell is the one provided as an argument.

If the provided existing object is nil, a single-element list will be returned.

Example of constructing a sequence in Clojure
1(def the-first  (cons 3 nil))          ; creates the list (3)
2(def the-second (cons 2 the-first))    ; creates cons(2)-->(3)
3(def the-last   (cons 1 the-second))   ; creates cons(1)-->cons(2)-->(3)
4
5(first the-last)         ; => 1     ; value in the last added cell
6(first (rest the-last))  ; => 2     ; value in the next cell
(def the-first (cons 3 nil)) ; creates the list (3) (def the-second (cons 2 the-first)) ; creates cons(2)--&gt;(3) (def the-last (cons 1 the-second)) ; creates cons(1)--&gt;cons(2)--&gt;(3) (first the-last) ; =&gt; 1 ; value in the last added cell (first (rest the-last)) ; =&gt; 2 ; value in the next cell

Using Cons we can join not only individual values but also collections (e.g., ordinary lists). We then have a compound structure that can be expressed, for example, like this:

1(cons (list 1 2 3)
2      (cons (list 4 5 6)
3            (cons 10
4                  ())))
5
6; => ((1 2 3) (4 5 6) 10)
(cons (list 1 2 3) (cons (list 4 5 6) (cons 10 ()))) ; =&gt; ((1 2 3) (4 5 6) 10)

Such structures are sometimes used to quickly join already grouped values into larger sets that are flattened only when being read in order to obtain individual values.

Example of flattening a nested sequence
1(flatten
2    (cons '(1 2 3) (cons '(4 5 6) (cons 10 nil))))
3
4; => (1 2 3 4 5 6 10)
(flatten (cons &#39;(1 2 3) (cons &#39;(4 5 6) (cons 10 nil)))) ; =&gt; (1 2 3 4 5 6 10)

See also:

“Cons cells”, chapter X.

Vectors

A vector is an ordered collection of values indexed using integers starting from 0 (the first element). In program code we use the vector literal to express vectors; we can also use several built-in functions.

Vectors in Clojure are represented by objects of type clojure.lang.PersistentVector. They are characterized by very fast data lookup and adding new elements to their ends. Vectors are also functions – they accept one argument, an index number, whose element value will be returned.

Examples of creating vectors
;; creating a vector (function)

(vector 1 2 3 4)
;=> [1 2 3 4]

;; creating a vector from an S-expression (literal)

[1 2 3 4]
;=> [1 2 3 4]

;; creating a vector from another collection

(vec (list 1 2 3 4))
;=> [1 2 3 4]

;; searching a vector (element at index 0)

([1 2 3 4] 0)
;=> 1
;; creating a vector (function) (vector 1 2 3 4) ;=&gt; [1 2 3 4] ;; creating a vector from an S-expression (literal) [1 2 3 4] ;=&gt; [1 2 3 4] ;; creating a vector from another collection (vec (list 1 2 3 4)) ;=&gt; [1 2 3 4] ;; searching a vector (element at index 0) ([1 2 3 4] 0) ;=&gt; 1

See also:

Maps

A map is an associative collection that stores mappings of keys to values. Keys and values may be data of any type, but the same key must be unique within a given map. Searching maps for a value identified by a key is very fast, as is adding new elements.

In Clojure, maps are represented by the following data types:

  • clojure.lang.PersistentHashMap (a map based on hash tables),
  • clojure.lang.PersistentArrayMap (a map based on plain arrays),
  • clojure.lang.PersistentTreeMap (a sorted map).

In program code we express maps using the map literal in the form of curly braces containing pairs of expressions. We can also use the hash-map function to express pairs as successive invocation arguments.

Examples of creating maps
;; creating a map (function)

(hash-map :a 1 :b 2)
;=> {:a 1 :b 2}

;; creating a sorted map (function)

(sorted-map :a 1 :b 2)
;=> {:a 1 :b 2}

;; creating a map from an S-expression (literal)

{:a 1 :b 2}
;=> {:a 1 :b 2}

;; creating a map from an S-expression (literal)

{:a 1 :b (+ 1 1)}
;=> {:a 1 :b 2}

;; creating a map from two sequential collections

(zipmap [:a :b] [1 2])
;=> {:a 1 :b 2}

;; creating a map from a sequence of pairs

(into {} [[:a 1] [:b 2]])
;=> {:a 1 :b 2}

;; creating a map from a sequence

(apply hash-map [:a 1 :b 2])
;=> {:a 1 :b 2}
;; creating a map (function) (hash-map :a 1 :b 2) ;=&gt; {:a 1 :b 2} ;; creating a sorted map (function) (sorted-map :a 1 :b 2) ;=&gt; {:a 1 :b 2} ;; creating a map from an S-expression (literal) {:a 1 :b 2} ;=&gt; {:a 1 :b 2} ;; creating a map from an S-expression (literal) {:a 1 :b (+ 1 1)} ;=&gt; {:a 1 :b 2} ;; creating a map from two sequential collections (zipmap [:a :b] [1 2]) ;=&gt; {:a 1 :b 2} ;; creating a map from a sequence of pairs (into {} [[:a 1] [:b 2]]) ;=&gt; {:a 1 :b 2} ;; creating a map from a sequence (apply hash-map [:a 1 :b 2]) ;=&gt; {:a 1 :b 2}

See also:

Sets

A set is a data structure that can store elements of arbitrary values with the restriction that a given value may appear in the set only once. Sets are characterized by very fast addition and lookup of elements. In Clojure there are built-in functions intended for performing operations on sets, e.g., join, projection, union, etc.

Sets are represented by objects of the following data types:

  • clojure.lang.PersistentSet (a set based on hash tables),
  • clojure.lang.PersistentTreeSet (a sorted set).
Examples of creating sets
;; creating a set (function)

(hash-set :a :b :c)
;=> #{:a :b :c}

;; creating a sorted set (function)

(sorted-set :a :b :c)
;=> #{:a :b :c}

;; creating a set from an S-expression (literal)

#{:a :b :c}
;=> #{:a :b :c}

;; creating a set from an S-expression (literal)

#{:a :b (keyword "c")}
;=> #{:a :b :c}

;; creating a set from a sequence

(set [:a :b :c])
;=> #{:a :b :c}
;; creating a set (function) (hash-set :a :b :c) ;=&gt; #{:a :b :c} ;; creating a sorted set (function) (sorted-set :a :b :c) ;=&gt; #{:a :b :c} ;; creating a set from an S-expression (literal) #{:a :b :c} ;=&gt; #{:a :b :c} ;; creating a set from an S-expression (literal) #{:a :b (keyword &#34;c&#34;)} ;=&gt; #{:a :b :c} ;; creating a set from a sequence (set [:a :b :c]) ;=&gt; #{:a :b :c}

See also:

Functions

A function is a construct containing code that, during invocation, computes a value for a given set of arguments. In purely functional languages this is the only result of using a function, while in others (including Clojure) functions may produce so-called side effects – that is, besides returning a value, they may affect their surroundings (e.g., modifying the value of a global variable or using the I/O subsystem).

Moreover, the result of a function’s operations in Clojure may depend on its surroundings. An example would be referencing global reference objects expressing some mutable state within its computations, or using values originating from the lexical environment of the function’s definition. In the latter case we call such a function a closure.

A function definition in Clojure consists of a list S-expression containing:

  • a vector S-expression defining the argument list,
  • one or more S-expressions constituting the function body.

In Clojure we can also use multi-arity functions, which can handle more than one set of arguments. In their case the definition syntax will be somewhat different.

The return value of a function is the computed value of the last S-expression from its body (or from the body of a given arity in the case of a multi-arity function).

In Clojure, to create anonymous functions we use the fn special form, and to create named functions we use the defn macro. Anonymous functions are function objects that have not been given names. Named functions in Clojure have names expressed through global variables.

Defining a function that displays text with an exclamation mark
1(defn shout           ; named function definition
2  [text]              ; vector with arguments
3  (print text "!"))   ; calling print with parameter text
4
5(shout "Hello, Lisp")    ; function call with argument
6;=>> Hello, Lisp!        ; result of call on screen
7;=> nil                  ; returned value
(defn shout ; named function definition [text] ; vector with arguments (print text &#34;!&#34;)) ; calling print with parameter text (shout &#34;Hello, Lisp&#34;) ; function call with argument ;=&gt;&gt; Hello, Lisp! ; result of call on screen ;=&gt; nil ; returned value

We can also create an anonymous version of the above function, but for the purposes of the example we must place the entire construct in an S-expression representing a function-call form so that we can observe the result of its execution:

Defining an anonymous function
1(                       ; function-call form
2  (fn                   ; anonymous function definition
3    [text]              ; vector with arguments
4    (print text "!"))   ; calling print with parameter text
5  "Hello, Lisp!")       ; function call argument
6
7;=>> Hello, Lisp!       ; result of call on screen
8;=> nil                 ; returned value
( ; function-call form (fn ; anonymous function definition [text] ; vector with arguments (print text &#34;!&#34;)) ; calling print with parameter text &#34;Hello, Lisp!&#34;) ; function call argument ;=&gt;&gt; Hello, Lisp! ; result of call on screen ;=&gt; nil ; returned value

The above can also be expressed using the anonymous function literal:

Using the anonymous function literal
1(                    ; function-call form
2  #(print %1 "!")    ; definition and calling print with first parameter
3  "Hello, Lisp!")    ; function call argument
4
5;=>> Hello, Lisp!    ; result of call on screen
6;=> nil              ; returned value
( ; function-call form #(print %1 &#34;!&#34;) ; definition and calling print with first parameter &#34;Hello, Lisp!&#34;) ; function call argument ;=&gt;&gt; Hello, Lisp! ; result of call on screen ;=&gt; nil ; returned value

Nothing prevents us from defining an anonymous function and then assigning it a name ourselves:

Defining a named function manually
1(def shout                ; global variable definition
2  (fn                     ; anonymous function definition
3    [text]                ; vector with arguments
4    (print text "!")))    ; calling print with parameter text
5
6(shout "Hello, Lisp")     ; function call with argument
7;=>> Hello, Lisp!         ; result of call on screen
8;=> nil                   ; returned value
(def shout ; global variable definition (fn ; anonymous function definition [text] ; vector with arguments (print text &#34;!&#34;))) ; calling print with parameter text (shout &#34;Hello, Lisp&#34;) ; function call with argument ;=&gt;&gt; Hello, Lisp! ; result of call on screen ;=&gt; nil ; returned value

In the above example we define a global variable shout with an initial value assigned to the result of invoking the fn special form, which will be a function object – a representation of an existing function.

In Lisp, functions are first-class citizens – values that can be treated like any other: passed as arguments or returned as results of function execution.

See also:

Boolean values

To express boolean values in Clojure we can use the singletons of type java.lang.Boolean: true denoting logical truth and false denoting logical falsehood. These values are returned by built-in predicates (functions that return logical truth or falsehood based on their arguments) and conditional constructs.

The nil Value

The literal nil denotes Clojure’s value for absence. On the JVM it is represented by Java null, but Clojure gives it explicit collection, sequence, equality, and truthiness semantics.

Functions may return nil when no more specific result is available, and many lookup operations use it for a missing value. In conditionals, nil and false are the only falsy values. Sequence functions also use nil to represent the absence of a sequence, including the end reached by next.

Comments

Comments in computer programs serve to explain fragments of source code in natural language. They may serve a documentary purpose or contain notes regarding progress on certain tasks. Sometimes they are also used to present the results of computing example forms.

In Clojure a comment begins with the semicolon character (;), after which we place arbitrary text. The comment will consist of any sequence of characters up to the end of the line in which it was started:

 1(print "Hello, Lisp!")  ; this is a comment
 2
 3;; This is a longer comment
 4;; written according to the convention
 5;; of preceding each line
 6;; with two semicolons.
 7
 8;; The comments below
 9;; are used to present computation results.
10
11(+ 2 2)  ; => 4
12(- 2 2)  ; => 0
13
14(+ 2 2)
15; => 4
16
17'(1 2 3)
18; => (1 2 3)
(print &#34;Hello, Lisp!&#34;) ; this is a comment ;; This is a longer comment ;; written according to the convention ;; of preceding each line ;; with two semicolons. ;; The comments below ;; are used to present computation results. (+ 2 2) ; =&gt; 4 (- 2 2) ; =&gt; 0 (+ 2 2) ; =&gt; 4 &#39;(1 2 3) ; =&gt; (1 2 3)

Comment form

Apart from syntactic comments expressed with the semicolon, Clojure provides a special macro that allows disabling the computation of a given S-expression:

(comment
    (print "Hello, Lisp!")
    123)
(comment (print &#34;Hello, Lisp!&#34;) 123)

It is worth noting that comment is a construct to which syntactically valid symbolic expressions must be passed as arguments.

The value returned by the macro is nil.

Ignoring the next form

The reader macro #_ causes the reader to skip the next complete form. The skipped form never reaches compiler analysis, which is stronger and earlier than merely suppressing its evaluation:

#_ (print "Hello, Lisp!")        ; the entire form is omitted
(#_#_ print "Hello, Lisp!")      ; two forms are omitted, leaving ()
(print #_ "Hello, Lisp!")        ; the string is omitted, leaving (print)
[#_#_ 1 2 3]                     ; 1 and 2 are omitted, leaving [3]
#_ (print &#34;Hello, Lisp!&#34;) ; the entire form is omitted (#_#_ print &#34;Hello, Lisp!&#34;) ; two forms are omitted, leaving () (print #_ &#34;Hello, Lisp!&#34;) ; the string is omitted, leaving (print) [#_#_ 1 2 3] ; 1 and 2 are omitted, leaving [3]

Control structures

Clojure is equipped with many special forms and macros that serve to control the expression-evaluation process, including the order of evaluation, the number of repetitions, and conditional processing.

Conditional expressions

The conditional statements known from imperative languages have counterparts in Lisps as conditional special forms or macros. Unlike an ordinary function call, these constructs control which operand forms are evaluated. This makes it possible to select a branch without evaluating the others.

Special forms and macros will be discussed later, and the fundamental difference between them is that macros are a language feature allowing source code transformations, whereas special forms are constructs built into the compiler. A macro expands to replacement forms that are analyzed again; a special form is handled directly according to its built-in grammar.

Conditional execution, if

The if special form controls conditional evaluation of expressions.

Usage:

  • (if predicate true-expression false-expression?).

The if form accepts two mandatory arguments. The first is a condition to be evaluated; if its value is neither false nor nil, the next argument will also be evaluated. If an optional third argument is provided, it will be evaluated only when the first argument has the value false or nil.

The if special form returns the value of the most recently evaluated expression, or nil if no third argument was provided and the first expression returned a logical falsehood.

Example of the if special form
1(if (= 2 2)
2  "equal"
3  "different")
4; => "equal"
(if (= 2 2) &#34;equal&#34; &#34;different&#34;) ; =&gt; &#34;equal&#34;

Together with the if form, the and macro is frequently used. It serves to express the logical conjunction (logical AND) operation. It evaluates the values of expressions given as its arguments (in the order they appear) as long as their value is logical truth (not false and not nil). The returned value is then the value of the last given expression. If any expression returns false or nil, processing of subsequent arguments is halted and its value is returned.

Example of the and macro
1(if (and (= 2 2) (< 2 3)) true)
2; => true
(if (and (= 2 2) (&lt; 2 3)) true) ; =&gt; true

Another macro found alongside if is or. It serves to express the logical disjunction (logical OR) operation. This macro evaluates the values of successive expressions given as its arguments (in the order they appear) until the value of one of them is logical truth (not false and not nil). The returned value is then the value of the most recently processed expression. If the values of all expressions are false or nil, the value of the last given expression is returned (false or nil).

Example of the or macro
1(if (or (= 2 2) (< 4 3)) true)
2; => true
(if (or (= 2 2) (&lt; 4 3)) true) ; =&gt; true

Conditional execution, if-not

The if-not macro is the inverse version of the if special form.

Usage:

  • (if-not predicate false-expression true-expression?).

The macro accepts two mandatory arguments. The first is a condition to be evaluated; if its value is logical falsehood (false or nil), the next argument will also be evaluated. If an optional third argument is provided, it will be evaluated only when the first argument represents logical truth (has a value different from false and different from nil).

The if-not macro returns the value of the most recently evaluated expression, or nil if no third argument was provided and the first expression returned logical truth.

Example of the if-not macro
1(if-not (= 2 2)
2  "different"
3  "equal")
4; => "equal"
(if-not (= 2 2) &#34;different&#34; &#34;equal&#34;) ; =&gt; &#34;equal&#34;

Conditional execution, when

The when macro works similarly to the if construct, but is somewhat simpler because there is no place for an alternative expression (one executed when the given condition is not met).

Usage:

  • (when predicate & expression...).

The first argument should be a form expressing a logical condition (one returning true or false, or possibly nil), and each remaining argument will be treated as an expression that will be evaluated when the value returned by the conditional construct is logical truth (a value different from false and different from nil).

Example of the when macro
1(when (= 2 2)
2  (println "you can provide")
3  (println "multiple expressions"))
4
5; >> you can provide
6; >> multiple expressions
(when (= 2 2) (println &#34;you can provide&#34;) (println &#34;multiple expressions&#34;)) ; &gt;&gt; you can provide ; &gt;&gt; multiple expressions

Conditional execution, when-not

The when-not macro is the inverse version of the when macro.

Usage:

  • (when-not predicate & expression...).

The first argument should be a predicate, and each remaining argument will be treated as an expression that will be evaluated when the predicate’s value is logical falsehood (false or nil).

Example of the when-not macro
1(when-not (= 2 1)
2  (println "you can provide")
3  (println "multiple expressions"))
4
5; >> you can provide
6; >> multiple expressions
(when-not (= 2 1) (println &#34;you can provide&#34;) (println &#34;multiple expressions&#34;)) ; &gt;&gt; you can provide ; &gt;&gt; multiple expressions

Condition list, cond

The cond macro allows writing a list of conditions with corresponding expressions that will be evaluated when a given condition is met.

Usage:

  • (cond & pair...),

where pair is:

  • predicate expression,
  • :else    default-expression.

The macro accepts an even number of arguments. For each pair it checks whether the given conditional expression returns a value different from false and from nil. If so, the associated argument from the pair (the expression) will be evaluated, processing of further arguments will stop, and the returned value will be the value of the evaluated expression.

Optionally, a final pair (option) can be added whose first element equals :else. Its associated expression will be evaluated when none of the earlier options terminated the process. This lets us set a default return value.

If no default option was given and no condition was met, the returned value will be nil.

Example of the cond macro
1(cond
2  (= 2 3) "2 and 3 are equal"
3  (< 2 3) "2 is less than 3"
4  :else   "neither")
5
6; => "2 is less than 3"
(cond (= 2 3) &#34;2 and 3 are equal&#34; (&lt; 2 3) &#34;2 is less than 3&#34; :else &#34;neither&#34;) ; =&gt; &#34;2 is less than 3&#34;

Case list, case

The case macro works similarly to cond, but instead of conditional expressions in the options, constant values should be given that will be matched against the value of the first argument provided.

Usage:

  • (case value option...),

where option is:

  • value      expression,
  • (value...) expression,
  • default-expression.

If we want to compare the given value against multiple values from a series for each possible option, that series of possibilities should be written as a list S-expression.

The values against which the first argument’s value will be compared must be compile-time constants. We may therefore provide literals or lists of literal test values, but not arbitrary expressions whose values are known only at runtime. The expressions to be evaluated upon a positive test may, in turn, represent arbitrary forms.

Optionally, a last argument can be provided as an expression that will be evaluated if the tested value does not satisfy any of the earlier conditions. It is worth using this possibility, because otherwise the program will throw an exception when no match is found.

Example of the case macro
1(def x 'cookies) ; x refers to the symbol cookies
2
3(case x
4  ""                    "nothing"
5  (mints dragees)       "sweets"
6  (cookies gingersnaps) "biscuits"
7  (str x " is an unknown product"))
8
9; => "biscuits"
(def x &#39;cookies) ; x refers to the symbol cookies (case x &#34;&#34; &#34;nothing&#34; (mints dragees) &#34;sweets&#34; (cookies gingersnaps) &#34;biscuits&#34; (str x &#34; is an unknown product&#34;)) ; =&gt; &#34;biscuits&#34;

The symbols and lists used as test constants are deliberately unquoted. case is a macro with its own syntax: those positions are compile-time test data, not ordinary function arguments.

Condition list with predicate, condp

The condp macro works similarly to cond, but is much more flexible.

Usage:

  • (condp predicate expression & pair...)

The first argument should be a predicate – a function that should return a boolean value – and the second an expression. The subsequent, optional arguments are options composed of pairs, which can be expressed using the following notations:

  • test-expression result-expression,
  • test-expression :>> result-function,
  • default-expression.

The symbolic notation :>> is a keyword that carries special meaning in the context of the condp macro.

For each given option, the associated function-call form will be evaluated:

  • (predicate test-expression expression).

The predicate will be invoked for each option given in order with two passed arguments. The first will be the value of the test expression of the given option, and the second will be the expression given as the argument to the entire macro invocation.

If the predicate returns a value corresponding to logical truth (different from false and different from nil), checking of subsequent options will stop and the result expression associated with the current option will be returned, or the value produced by invoking the result function (in the case where the :>> keyword was used). In this latter case, the value returned by the predicate will be passed to the function (which should accept one argument).

If a final additional expression was given as the last option (default-expression), its value will be returned if none of the options matched the given data. When such a situation occurs but no default expression was provided, an IllegalArgumentException will be thrown.

Example of the condp macro
 1(condp
 2  =           ; predicate
 3  (+ 2 2)     ; expression (yields 4)
 4  1 "one"     ; option 1: string
 5  2 "two"     ; option 2: string
 6  3 "three"   ; option 3: string
 7  4 "four"    ; option 4: string
 8  "none")     ; default option expression
 9
10; => "four"
(condp = ; predicate (+ 2 2) ; expression (yields 4) 1 &#34;one&#34; ; option 1: string 2 &#34;two&#34; ; option 2: string 3 &#34;three&#34; ; option 3: string 4 &#34;four&#34; ; option 4: string &#34;none&#34;) ; default option expression ; =&gt; &#34;four&#34;

Java member access

Member access, .

The . special form in Clojure provides access to Java objects. It is de facto a member-access operator.

  • When the target is a symbol naming a Java class, the compiler treats the notation as access to a static member of that class.

  • Otherwise the target expression is evaluated to an instance. The member syntax may denote a field or a method; prefixing the member name with a hyphen (-) requests field access explicitly. Type hints can help the compiler resolve ambiguous or overloaded host members without reflection.

  • If additional arguments are given beyond the first, a method of the instance with the name given as the first argument will be invoked. The additional arguments will (from left to right) be passed to the invocation after their values are evaluated.

Additional arguments may also be a list S-expression. In such a case, the first element should be a symbol identifying the method to invoke.

There is also shorthand syntax in which the dot prefixes the member name in operator position, for example (.trim value).

Examples of the . special form
(. " a " trim)             ; => a
(.trim " a ")              ; => a
(. "abc" substring 0 1)    ; => a
(. "abc" (substring 0 1))  ; => a
(. &#34; a &#34; trim) ; =&gt; a (.trim &#34; a &#34;) ; =&gt; a (. &#34;abc&#34; substring 0 1) ; =&gt; a (. &#34;abc&#34; (substring 0 1)) ; =&gt; a

Member access, ..

The macro named .. is syntactic sugar that allows expressing nested access to Java object members in a readable way. The effect of using it is the creation of a cascade of invocations of the . special form discussed above.

In practice, .. performs a series of method calls in which the result of one becomes the input object for the next. Each expression after .. is interpreted similarly to the . form, but without the need for manual nesting.

Examples of the .. macro
(.. " a " trim length)                             ; => 1
(.. " a " trim length toString (replace "1" "X"))  ; => "X"
(.. &#34; a &#34; trim length) ; =&gt; 1 (.. &#34; a &#34; trim length toString (replace &#34;1&#34; &#34;X&#34;)) ; =&gt; &#34;X&#34;

Code organization

Clojure forms are expressions and produce values, but evaluating an expression may also perform effects. Pure transformations and explicit effectful boundaries can therefore coexist without pretending that every useful operation is pure.

Grouping S-expressions, do

Perhaps the most common construct that lets us momentarily forget that we are dealing with evaluated expressions and treat program elements as statements is the do special form. It serves to group multiple expressions in such a way that only the value of the last one is returned, although side effects (e.g., displaying text on screen) may occur in all of them.

Usage:

  • (do & expression...).

The do special form accepts zero or more S-expressions, which will be evaluated in the order they are given. The returned value will be the result of evaluating the last one.

If no S-expression is given, the returned value will be nil.

Examples of the do special form
 1(do)
 2; => nil
 3
 4(do 123 456)
 5; => 456
 6
 7(do
 8  (println "test")
 9  123
10  (+ 1 1)
11  (+ 2 2))
12; >> test
13; => 4
(do) ; =&gt; nil (do 123 456) ; =&gt; 456 (do (println &#34;test&#34;) 123 (+ 1 1) (+ 2 2)) ; &gt;&gt; test ; =&gt; 4

Appending an argument, doto

The doto macro evaluates its target once, inserts that result after the operator in each following list form, evaluates those forms for their effects, and returns the original target. It is most commonly used for a sequence of Java method calls on one object.

  • (doto value & expression...).

The macro accepts one mandatory target expression. Each subsequent list is transformed so that the target value becomes its first argument.

The value returned by doto is the evaluated target value.

Examples of the doto macro
 1(doto 2
 2  (println "the first time")
 3  (println "the second time"))
 4
 5; >> 2 the first time
 6; >> 2 the second time
 7; => 2
 8
 9(doto (java.util.ArrayList.)
10  (.add 1)
11  (.add 2)
12  (.add 3))
13
14; => [1 2 3]
(doto 2 (println &#34;the first time&#34;) (println &#34;the second time&#34;)) ; &gt;&gt; 2 the first time ; &gt;&gt; 2 the second time ; =&gt; 2 (doto (java.util.ArrayList.) (.add 1) (.add 2) (.add 3)) ; =&gt; [1 2 3]

Timing, time

The time macro allows us to display the execution time of a given expression. It is useful when we want to optimize parts of a program for performance.

Usage:

  • (time expression).

The macro’s argument should be an S-expression whose value will be computed. do can be used if we want to provide more expressions.

The value returned by the time macro is the value of the evaluated expression that was given.

A side effect of invoking the macro is the display of an execution-time report on standard output.

Examples of the time macro
1(time 123)
2; >> "Elapsed time: 0.012667 msecs"
3; => 123
4
5(time (Thread/sleep 500))
6; >> "Elapsed time: 505.079375 msecs"
7; => nil
(time 123) ; &gt;&gt; &#34;Elapsed time: 0.012667 msecs&#34; ; =&gt; 123 (time (Thread/sleep 500)) ; &gt;&gt; &#34;Elapsed time: 505.079375 msecs&#34; ; =&gt; nil

Threading, ->

The threading macro, also called the thread-first macro, enables presenting source code with nested invocations in a more readable way. It works by accepting a set of S-expressions and treating each one except the first as a call form (even if it is not enclosed in parentheses), and then inserting in the position of the first argument of each form the value returned by the preceding invocation. Thanks to this we can momentarily forget about Polish notation.

Usage:

  • (-> expression & expression...).

The macro accepts an initial expression and zero or more call forms. Its result is the value of the fully expanded call chain.

Example of the -> macro
1(-> [1 2 3 4]  ; result [1 2 3 4]
2    last       ; result (last [1 2 3 4]) => 4
3    (- 2)      ; result (- 4 2)          => 2
4    println    ; result (println 2)      => nil
5)
6
7; >> 2
(-&gt; [1 2 3 4] ; result [1 2 3 4] last ; result (last [1 2 3 4]) =&gt; 4 (- 2) ; result (- 4 2) =&gt; 2 println ; result (println 2) =&gt; nil ) ; &gt;&gt; 2

Let us look at the same operations arranged in code without using the macro:

1(println
2  (- (last
3      [1 2 3 4]))
4     2)
5
6; => nil
7; >> 2
(println (- (last [1 2 3 4])) 2) ; =&gt; nil ; &gt;&gt; 2

Let us compare both notations in more compact forms:

1(println (- (last [1 2 3 4])) 2)   ; nested notation
2(-> [1 2 3 4] last (- 2) println)  ; linear data-flow notation
(println (- (last [1 2 3 4])) 2) ; nested notation (-&gt; [1 2 3 4] last (- 2) println) ; linear data-flow notation

Both forms expand to the same nested calls and obey the same evaluation rules. The difference is source layout: -> presents the data flow from left to right by inserting each intermediate form as the first argument of the next call.

Thread-last, ->>

The thread-last macro works similarly to the threading macro, but the value of each preceding S-expression will be substituted not as the first but as the last argument of the next one.

Usage:

  • (->> expression & expression...).

The macro accepts an initial expression and zero or more call forms. Its result is the value of the fully expanded call chain.

Example of the ->> macro
1(->> [1 2 3 4]  ; result [1 2 3 4]
2     last       ; result (last [1 2 3 4]) => 4
3     (- 2)      ; result (- 2 4)          => -2
4     println    ; result (println -2)     => nil
5)
6
7; => nil
8; >> -2
(-&gt;&gt; [1 2 3 4] ; result [1 2 3 4] last ; result (last [1 2 3 4]) =&gt; 4 (- 2) ; result (- 2 4) =&gt; -2 println ; result (println -2) =&gt; nil ) ; =&gt; nil ; &gt;&gt; -2

In the above example we can see that the difference in computations compared to the previous examples appears in line 3. The subtraction arguments are swapped because the value is substituted as the second argument of the subtraction.

Threading non-nils, some->

The some-> macro works similarly to the -> macro, but threading and further evaluation of expressions stop immediately when any form in the chain returns nil. It is useful when, for example, a lookup may return nil and subsequent operations must not be invoked on that value.

Usage:

  • (some-> expression & expression...).

The macro accepts an initial expression and zero or more call forms. It returns the final threaded result, or nil as soon as an intermediate result is nil.

Example of the some-> macro
1(some-> [1 2 3 4]  ; result [1 2 3 4]
2        (get 50)   ; result (get [1 2 3 4] 50)  => nil
3        (+ 2)      ; not evaluated
4        println    ; not evaluated
5)
6
7; => nil
(some-&gt; [1 2 3 4] ; result [1 2 3 4] (get 50) ; result (get [1 2 3 4] 50) =&gt; nil (+ 2) ; not evaluated println ; not evaluated ) ; =&gt; nil

We can see that in line 2 further evaluation of S-expressions in the chain was halted because the form (get [1 2 3 4] 50) returned nil (when attempting to access a nonexistent element at index 50). Let us see what would happen if we used -> instead of some->:

1(-> [1 2 3 4]  ; result [1 2 3 4]
2    (get 50)   ; result (get [1 2 3 4] 50)  => nil
3    (+ 2)      ; result (+ nil 2)  (exception)
4    println    ; not evaluated (exception)
5)
6
7; >> java.lang.NullPointerException:
(-&gt; [1 2 3 4] ; result [1 2 3 4] (get 50) ; result (get [1 2 3 4] 50) =&gt; nil (+ 2) ; result (+ nil 2) (exception) println ; not evaluated (exception) ) ; &gt;&gt; java.lang.NullPointerException:

Instead of short-circuiting and returning nil, evaluation throws an exception when + receives nil. A REPL reports the exception and remains available; an application’s surrounding error-handling policy determines what happens next.

Threading non-nils, some->>

The some->> macro works similarly to some->, but the values of previously evaluated S-expressions are substituted as the last arguments of subsequent ones.

Usage:

  • (some->> expression & expression...).

The macro accepts an initial expression and zero or more call forms. It returns the final threaded result, or nil as soon as an intermediate result is nil.

Example of the some->> macro
1(some->> [2 4 6 8]      ; result [2 4 6 8]
2         (filter odd?)  ; result (filter odd? [2 4 6 8]) => ()
3         first          ; result (first ()) => nil
4         (+ 2)          ; not evaluated
5         println        ; not evaluated
6)
7
8; => nil
(some-&gt;&gt; [2 4 6 8] ; result [2 4 6 8] (filter odd?) ; result (filter odd? [2 4 6 8]) =&gt; () first ; result (first ()) =&gt; nil (+ 2) ; not evaluated println ; not evaluated ) ; =&gt; nil

In line 2 we used the not-yet-discussed function filter. It filters the collection given as the second argument, and to decide whether an element will be included in the output data structure or skipped it uses the predicate passed as the first invocation argument.

Here the role of the predicate is played by the odd? function, which returns logical truth when the value of its argument is an odd number. As a result, filter will produce an empty collection because the input contains only even numbers.

Invoking first on an empty collection returns nil, and at that point threading ends. Had we not used the some->> macro, the S-expression (+ 2 nil) would have led to an exception being thrown during evaluation.

Conditional threading, cond->

The cond-> macro works similarly to ->, but S-expressions are interpreted in pairs. The first of each pair is a conditional expression, and the second will be evaluated only when the first is truthy (its value is neither false nor nil). Each second expression of a pair is treated as a call form even if it is not enclosed in parentheses.

Conditional threading of the first argument is useful for clearly expressing a set of computations that depend on current conditions.

Usage:

  • (cond-> expression & pair...),

where pair is:

  • predicate expression.

The cond-> macro accepts an initial expression and pairs of S-expressions, where the first is a predicate and the second will be evaluated in such a way that the result of evaluating the previous expression in the chain is inserted as the first argument. The returned value is the value of the most recently evaluated S-expression.

Example of the cond-> macro
 1(def add-one?      true)
 2(def subtract-one? false)
 3
 4(cond-> 123              ; result 123
 5        add-one?      inc     ; result (inc 123) => 124
 6        subtract-one? dec     ; not evaluated
 7        true          println ; result (println 124) => nil
 8)
 9
10; => nil
11; >> 124
(def add-one? true) (def subtract-one? false) (cond-&gt; 123 ; result 123 add-one? inc ; result (inc 123) =&gt; 124 subtract-one? dec ; not evaluated true println ; result (println 124) =&gt; nil ) ; =&gt; nil ; &gt;&gt; 124

Conditional threading, cond->>

The conditional thread-last macro works similarly to cond->, but – as the name suggests – the value of the most recently evaluated S-expression is substituted in the position of the last argument of the currently evaluated expression.

Conditional threading of the last argument is useful for clearly expressing transformations performed on collections, since many built-in functions operating on such data structures accept them as their last arguments.

Usage:

  • (cond->> expression & pair...),

where pair is:

  • predicate expression.

The cond->> macro accepts an initial expression and pairs of S-expressions, where the first is a predicate and the second will be evaluated in such a way that the result of evaluating the previous expression in the chain is inserted as the last argument. The returned value is the value of the most recently evaluated S-expression.

Example of the cond->> macro
 1(def evens? true)
 2(def odds? false)
 3
 4(cond->> [1 2 3 4]                   ; [1 2 3 4]
 5         evens?    (filter even?)    ; (filter even? [1 2 3 4]) => (2 4)
 6         odds?     (filter odd?)    ; not evaluated
 7         true      println          ; (println '(2 4)) => nil
 8)
 9
10; => nil
11; >> (2 4)
(def evens? true) (def odds? false) (cond-&gt;&gt; [1 2 3 4] ; [1 2 3 4] evens? (filter even?) ; (filter even? [1 2 3 4]) =&gt; (2 4) odds? (filter odd?) ; not evaluated true println ; (println &#39;(2 4)) =&gt; nil ) ; =&gt; nil ; &gt;&gt; (2 4)

Binding to a name, as->

The as-> macro threads expression results, but instead of a fixed position we are dealing with the creation of bindings to a symbolic name. This is useful in situations where different invocations require placing the significant argument at different positions.

Usage:

  • (as-> expression symbolic-name & expression...).

The macro’s arguments are an initial expression whose value will be bound to the symbolic name (given as the second argument), and then threaded through the subsequent S-expressions in such a way that the value of each is bound to the symbolic name before the next one is invoked.

Example of the as-> macro
1(as-> [1 2 3 4] result       ; after substituting result:
2      (filter even? result)  ; (filter even? [1 2 3 4])
3      (last result)          ; (last [2 4])
4      [result (inc result)]) ; [4 5]
5
6; => [4 5]
(as-&gt; [1 2 3 4] result ; after substituting result: (filter even? result) ; (filter even? [1 2 3 4]) (last result) ; (last [2 4]) [result (inc result)]) ; [4 5] ; =&gt; [4 5]

Summary

Translating source code

The diagram below presents the individual stages of translating a program’s source code from its textual form into an executable one, including the process of running and obtaining expression values.

Translation of a program to executable form in Clojure

Questions and answers

Let us answer a few questions that will help summarize the introduction to the basic mechanisms present in Lisps (and in Clojure):

  • What is the difference between an S-expression and a list?

    An S-expression is a textual notation for symbolic data. A list is an in-memory sequential data structure. Reading a parenthesized S-expression normally produces a list, which may later be evaluated as a form or handled as ordinary data.

  • What is the difference between an S-expression and an atom?

    In traditional Lisp terminology, an atom is a non-compound S-expression. This should not be confused with Clojure’s Atom reference type.

  • What is the difference between an S-expression and a form?

    A form is a Clojure value presented for evaluation. It may have originated in an S-expression read from text, or it may have been constructed directly as data.

  • What is the difference between a symbol and a global variable?

    A namespace object maps a symbol to a Var. The Var supplies a root binding and, when dynamic rebinding is enabled, possibly a current thread binding. The symbol names the Var but does not itself store a reference to its value.

See also:

Current section: Read Me Clojure
Categories:

Taxonomies: