Bindings let source-level names denote values and reference objects, while namespaces organize top-level Vars, Java classes, and aliases. In this installment, we will learn how these mechanisms shape resolution, scope, and visibility in Clojure.
Bindings and Namespaces
An important element of computer programming is managing identifiers, that is, human-readable labels that allow us to refer to objects placed in memory.
Bindings
A binding, also called name binding, associates an identifier with a value
or with an object through which a value can be obtained. For example, a lexical name
a can denote the immutable value 123, while a namespace name can resolve to a
Var whose current binding contains that value.
(def a 123) ; global binding
(let [a 123] a) ; lexical binding
Thanks to name bindings, we can refer to data placed in memory using readable identifiers in the source code of computer programs instead of, say, memory addresses. Beyond that, they open the way to abstracting data structures, meaning making access to them independent of the hardware architecture.
Bindings decouple program meaning from storage layout. A lexical binding is immutable, whereas a stable reference identity may expose different values over time according to its update contract. This distinction is discussed in more detail in chapter XIII.
Examples of binding names to values might bring to mind operating on variables, but creating an association of a value with an identifier does not necessarily mean that the contents of the memory area where the value resides can be changed. The process of name binding is therefore something more elementary than creating variables and may be a step within that process.
Early versus late binding describes when a name or operation is resolved. Lexical versus dynamic scope describes where a binding is selected. These are independent axes and should not share one overloaded “static/dynamic” explanation. In Clojure, compiler analysis resolves lexical names and Vars, while Var dereferencing and protocol or multimethod dispatch may still select runtime values or implementations.
Software engineering distinguishes two important properties that determine how and where we can use bindings and the values they point to:
- scope,
- visibility.
See also:
- “Handling bindings” in this chapter.
Scope
The scope of a binding (of an identifier with an in-memory object) is the part of the program in which we can use that binding to refer to the object using the name assigned to it.
The part of the program will most often be a lexical fragment of the source code (e.g., a block, module, function, source code file, etc.), but it may also be considered dynamically and depend on the state of program execution at a given point in time.
A scope determined by source structure is lexical. A Clojure dynamic Var
binding is selected from the current thread’s stack-disciplined call context. A
function may therefore refer to a dynamic Var defined elsewhere and observe the
innermost active binding established by its caller. Lexical closures, by contrast,
capture lexical bindings; that distinction does not mean that every language freezes
all captured objects at function-definition time.
In Clojure the following kinds of binding scopes are supported:
- lexical – depends on the identifier’s placement in the source code,
- dynamic – depends on the calling context of dynamic variables,
- namespace/global – expressed by global Vars and constrained by qualification and visibility rules.
1;; lexical scope
2;; limited to the body of the let form
3
4(let [a 123] ; binding a symbol to a value
5 a) ; using the value identified by the symbol
6
7;; namespace-level definition
8
9(def a 123) ; binding a global Var to a value
10a ; using the current value identified by the symbol
11
12;; dynamic scope
13
14(def ^:dynamic *a* 456) ; root binding of a dynamic Var
15*a* ; using the current value
16(binding [*a* 789] ; installing a temporary thread binding
17 *a*) ; using the dynamically selected value
;; lexical scope
;; limited to the body of the let form
(let [a 123] ; binding a symbol to a value
a) ; using the value identified by the symbol
;; namespace-level definition
(def a 123) ; binding a global Var to a value
a ; using the current value identified by the symbol
;; dynamic scope
(def ^:dynamic *a* 456) ; root binding of a dynamic Var
*a* ; using the current value
(binding [*a* 789] ; installing a temporary thread binding
*a*) ; using the dynamically selected value
Visibility
Visibility is the part of the program in which we can access and use the value associated with an identifier and stored in a memory structure. We will say, then, that (depending on the language) the following is visible: a variable, a constant, a value, or another construct responsible for storing data.
An example of the difference between visibility and scope is a situation where an
in-memory object (e.g., the integer 2) identified by a symbolic name (e.g., x) in
a certain lexical region (e.g., inside a function definition) ceases to be
visible because the name x was used as a function parameter whose purpose is to
refer to the value of the passed call argument. The value 2 does not disappear, but
its identification is temporarily shadowed within the function definition
region. When the source code fragment defining the function ends, x will still
refer to the value 2. We will therefore speak of the scope of x (or of the binding
of x to a value), but of the visibility of the value 2 bound to x.
The visibility of an identified object will never be greater than the scope of the binding, but the scope of the binding may be greater than the visibility – as in the example above.
In Clojure we can control visibility by using namespaces and designating global identifiers as private or public, while scope depends on the kind of constructs used and on the context.
Namespaces
In Clojure the namespace mechanism lets us organize program elements and separate symbolic identifiers that originate from different sources in order to avoid naming conflicts.
A Clojure namespace is a uniquely named object that manages mappings from symbols to Vars and Java classes, together with aliases to other namespaces. It is a name- resolution context, not a general-purpose map and not a container for the values themselves.
Top-level definitions use namespaces because their symbols are interned as mappings to Vars there. Lexical bindings, by contrast, are represented in the compiler’s lexical environment and do not become namespace mappings.
Current Namespace
How does the compiler know which namespace is in question? First, it can use the currently set current namespace. When we look at the source code file of any program, we will see at the beginning a notation similar to the following:
(ns smangler.api
(:require [clojure.string :as string]
[clojure.set :refer :all]))
(ns smangler.api
(:require [clojure.string :as string]
[clojure.set :refer :all]))
It tells the compiler that during further loading of the source code the dynamic Var
*ns* should have the value of a clojure.lang.Namespace object. If the namespace
with the given name (in this example smangler.api) does not yet exist, it will be
created. Top-level definitions subsequently analyzed in this context are interned as
Vars in that namespace unless they explicitly target another permitted context.
The following code will cause a binding of the symbol a to an object of type Var
referring to the value 1 to appear in the current namespace (e.g., smangler.api):
(def a 1)
(def a 1)
During analysis, an unqualified symbol that is neither a special form nor a lexical binding is resolved through mappings of the current namespace. Qualified symbols, class names, and host members follow their own resolution rules.
The macro ns supports quite a few additional options, but :require is the
most commonly used. It causes so-called aliases to be created in the namespace, which
allow referring to other namespaces (the :as option). It can also be used to place
bindings from another namespace into the current one so that symbols do not need to be
prefixed with its name (the :refer option).
We can see, then, that when bringing functions or global Vars into existence in
Clojure, the programmer must choose in which namespace their symbolic identifiers
will reside. The programmer is then able to refer to two different constructs with
the same names but registered in different namespaces, e.g., the function sum from
the namespace light-years and the function sum from the namespace
rational-numbers (using the notation light-years/sum and
rational-numbers/sum). The programmer may also decide that certain bindings
originating from other namespaces should be referred in the current namespace if
their names are sufficiently unique, and may even rename them.
Namespace-Qualified Symbols
We can also refer to global identifiers located in namespaces using a special symbolic form in which, in addition to the proper identifying name, we also find the namespace name, e.g.:
(clojure.core/println 1 2 3)
(clojure.core/println 1 2 3)
The above notation means that we want to call the function identified by the name
println from the namespace clojure.core. To locate the function’s subroutine, the
compiler will use the namespace we specified instead of the one indicated by the
current value of the variable *ns*.
Namespace Structure
In a more systematic way, we will characterize a namespace as an object (of type
clojure.lang.Namespace):
-
serving to maintain:
- mappings from identifiers to global Vars,
- mappings from identifiers to Java classes,
- aliases referring to other namespaces;
-
containing two associative structures:
- an aliases map,
- a mappings map;
-
registered in a global repository at the moment of creation so that language mechanisms can use it during the automatic resolution of symbolic identifiers to their corresponding in-memory objects holding values.
The function of the aliases map present in a namespace is to create references to other namespaces using alternative identifiers assigned to them in the form of symbols.
The namespace aliases map associates symbols with objects of other namespaces
Instead of prefixing every symbol name with a long namespace name (e.g.,
io.randomseed.blog.calc/add), we can create an alias for it in the current
namespace (e.g., calc) and use a shortened version when referring to identified
values or functions (e.g., calc/add).
The mappings map contains pairs whose keys are symbols and whose values can be:
- objects of type
Var, - Java
Classobjects.
It is worth noting that an object of type Var placed in a namespace is called
a global Var. Global Vars in Clojure are used to identify
configuration information or subroutines (e.g., functions or macros).
The namespace mappings map associates symbols with Var objects or Java classes
Among the values found in the mappings map we can find not only Vars created and assigned to the same namespace, but also objects residing in other namespaces.
Mappings to Var objects from other namespaces are created using
refer or use, mappings to Java classes using import,
while new global Vars are automatically mapped at the moment of their creation
(e.g., using def or intern).
Public and Private Vars
An interned Var can carry private metadata. Public referring operations omit private Vars, and compiler resolution rejects ordinary access from another namespace. Privacy belongs to the Var metadata rather than to a separate kind of namespace-map entry. In practice:
-
They can be referred to from inside functions that were defined in the same namespace.
-
They can be referred to when the currently set namespace is the one they belong to. This is an additional way of controlling visibility.
Namespace Initialization
When the Clojure runtime environment is being prepared, the clojure.core namespace is
initialized. The symbol *ns* identifies a global
dynamic variable defined in clojure.core, and its current
value points to the current namespace object. In a REPL session this value usually
points to the user namespace.
Public Vars from clojure.core are referred into the current namespace, which makes
core functions and macros available without namespace qualification. Special forms are
handled directly by the compiler and are not stored as namespace mappings.
Among the automatically added references, we will find, among others, those that identify the following operations:
-
in-ns– used to set the current namespace, -
import– a macro that maps selected Java class names into the current namespace, -
refer– assigns symbols toVarobjects from another namespace in the current namespace.
Thanks to this, the programmer can further extend the namespace’s contents with new mappings.
The functions presented further on provide more or less general access to namespaces.
In practice, however, they are rarely used directly; instead, one uses the
ns macro.
Creating Namespaces
To use a given namespace, it must first be created. Some namespaces are created
automatically, e.g., clojure.core and user (when using the
REPL). However, if we wanted to create a namespace on our own, for example to
encapsulate a library we are building or to better manage visibility in an
application, we can use the appropriate function.
Creating a Namespace, create-ns
The function create-ns takes a namespace name in the form of a literal symbol and
creates the given namespace if it does not yet exist. If a namespace with the given
name has already been created, no action is taken.
Usage:
(create-ns symbolic-name).
The function takes a namespace name expressed as a symbol value and returns a
namespace object (of type clojure.lang.Namespace). In source code, that symbol is
usually quoted so that it is passed as data rather than resolved.
create-ns function
(create-ns 'new-namespace)
; => #<Namespace new-namespace>
See also:
Using Namespaces
Using namespaces involves resolving symbol forms to mapped Vars or Java classes.
Compiler resolution follows semantics equivalent to the public resolve and
ns-resolve operations where applicable, but it need not literally call those Vars as
ordinary functions during compilation.
Let us recall that symbols can have a qualified namespace or contain no namespace information. This property lets us use them in two ways and translates into different modes of referring to namespaces.
When we provide a symbol without specifying a namespace in it (e.g., replace), the
current namespace will be searched. If, on the other hand, we specify a namespace
in the symbol (e.g., clojure.string/replace), the specified namespace will be
searched.
Resolution yields either a Var or a Java class mapping. Evaluating a symbol that
resolves to a Var obtains the Var’s current binding; asking resolve directly returns
the Var object itself.
Setting the Current Namespace, in-ns
The current namespace is held by the dynamic Var *ns*. We are therefore able, within
a given source file or REPL context, to switch the namespace by changing its current
thread binding. A common practice is
to set the current namespace at the beginning of a source file whose entire set of
function, macro, and global Var definitions will, during its loading, register global
bindings in the chosen namespace by default.
To avoid having to think about how the information about the current namespace is
stored, and thus about the methods of operating on it, we can use a ready-made function
designed for switching the current namespace. It is called in-ns.
Usage:
(in-ns symbolic-name).
The function takes one argument, which should be a quoted symbol. If the
namespace designated by it does not yet exist, the function create-ns will be called
to create it. The function sets the current binding of *ns* and returns the namespace
object.
in-ns function
(in-ns 'new-namespace)
; => #<Namespace new-namespace>
After executing the above code in the REPL, we may be surprised that the compiler
does not “see” language functions that were previously reachable by providing their
symbolic identifiers. This happens because we did not refer into the newly created
namespace the symbol bindings present in the user namespace, which the REPL uses by
default.
To use names bound to global Vars from another namespace, one must use the (discussed
further on) function refer, which will create appropriate
references. There is also a convenient macro ns, which will be discussed
later.
Reading a Namespace Component, namespace
The function namespace returns the namespace component of a symbol or keyword.
Usage:
(namespace symbolic-name).
This function does not perform namespace resolution; it reads the component stored in the supplied named value.
The return value is a string, or nil when the value is unqualified.
namespace function
(namespace 'namespace/some-name)
; => "namespace"
(namespace :example/key)
; => "example"
Resolving by Names, ns-resolve
The function ns-resolve resolves a symbol against the namespace supplied as its
first argument (a namespace symbol or object). It returns the mapped Var or Java
Class, not the Var’s current value, and returns nil when no mapping is found.
The namespace being searched must exist; if it does not, an exception will be thrown.
In the three-argument variant, the function takes as its second argument the name of
a so-called environment. This can be any collection on which the contains?
function can be called (e.g., a set or a map). If the given symbol
is found in the environment, the namespace will not be searched and the function will
return nil.
Usage:
(ns-resolve namespace symbolic-name),(ns-resolve namespace environment symbolic-name).
ns-resolve function
1(ns-resolve 'user 'replace) ; => #'clojure.core/replace
2(ns-resolve *ns* 'replace) ; => #'clojure.core/replace
3(ns-resolve 'clojure.string 'replace) ; => #'clojure.string/replace
4(ns-resolve 'clojure.string 'something) ; => nil
5(ns-resolve *ns* #{'replace 'something} 'replace) ; => nil
(ns-resolve 'user 'replace) ; => #'clojure.core/replace
(ns-resolve *ns* 'replace) ; => #'clojure.core/replace
(ns-resolve 'clojure.string 'replace) ; => #'clojure.string/replace
(ns-resolve 'clojure.string 'something) ; => nil
(ns-resolve *ns* #{'replace 'something} 'replace) ; => nil
Resolving in the Namespace, resolve
The function resolve is a less demanding version of ns-resolve. It does not
require specifying a namespace name because it uses the current one by default
(determined by *ns*). A namespace-qualified symbol directs resolution to the named
or aliased namespace. The return value may be a Var, a Java Class, or nil.
Usage:
(resolve symbolic-name),(resolve environment symbolic-name).
resolve function
1(resolve 'replace)
2; => #'clojure.core/replace
3
4;; providing an environment that contains the symbol
5
6(resolve #{'replace} 'replace)
7; => nil
8
9;; providing a symbol with a qualified namespace
10
11(resolve 'clojure.string/replace)
12; => #'clojure.string/replace
(resolve 'replace)
; => #'clojure.core/replace
;; providing an environment that contains the symbol
(resolve #{'replace} 'replace)
; => nil
;; providing a symbol with a qualified namespace
(resolve 'clojure.string/replace)
; => #'clojure.string/replace
Managing Namespaces
Thanks to the functions all-ns and find-ns, we can search for namespaces and
retrieve their lists.
Retrieving the List of Namespaces, all-ns
The function all-ns takes no arguments and returns a sequence of all currently
registered namespace objects. The concrete sequence class is an implementation
detail.
Usage:
(all-ns).
all-ns function
(all-ns)
; => (#<Namespace reply.main> #<Namespace clojure.tools.nrepl.misc> ...)
Finding a Namespace, find-ns
The function find-ns returns the namespace object whose name, specified by a symbol
value, was given as its first argument. If a namespace with the given name
does not exist, the value nil is returned.
Usage:
(find-ns namespace-name).
find-ns function
(find-ns 'user)
; => #<Namespace user>
Removing a Namespace, remove-ns
Namespaces can not only be added but also removed. This operation is possible using
the function remove-ns. It takes one argument, which is the name of the namespace to
be removed expressed as a symbol value.
The function returns nil when the namespace does not exist, or the namespace object
when removal was performed. It cannot be used to remove the clojure.core namespace.
Usage:
(remove-ns namespace-name).
remove-ns function
(create-ns 'temporary.example)
(some? (remove-ns 'temporary.example))
; => true
Managing Mappings
Bindings of symbols to global Vars or Java objects can be added to namespaces using one of several functions designed for this purpose.
Adding References to Vars, refer
The function refer adds references to Var objects placed in other
namespaces within the current namespace. As its first argument it takes the name of
the source namespace as a symbol value, and as subsequent, optional
arguments, filters that will be used to refine the operations performed.
The function returns nil, and in the case of a nonexistent namespace, an exception
is thrown.
Usage:
(refer namespace-name & filters).
As a result of calling refer, all symbols pointing to Var objects will be
retrieved from the given namespace, and then references with the same symbolic names
will be created in the current namespace, unless appropriate filters were used.
In the case where a reference being created has the same symbolic name as an already existing mapping, it will be overwritten, and an appropriate warning will be sent to the standard diagnostic output.
In the current namespace, Var objects residing in the source namespace will be
directly associated with symbols. When a binding in the original namespace is removed
(e.g., using ns-unmap), the current namespace will still contain the
mapping of the symbol to the Var object.
The available filters are:
:exclude symbol-sequence– symbols to skip,:only symbol-sequence– symbols to process exclusively,:rename symbol-map– symbols to rename.
These filters should consist of a name in the form of a keyword, followed
by a sequence of symbols specifying the filter values, which will be
symbolic names to include (:only) or skip (:exclude).
The sequences can be any collections (e.g., vectors) equipped with
a sequential access interface. If renaming was requested (:rename), then instead of
a sequence one should provide a map containing transformation pairs. In this
way, a given global Var can be referred to in the current namespace under a different
symbolic name.
refer function
1;; create references to all Vars from clojure.string
2
3(refer 'clojure.string)
4; => nil
5
6;; create a reference only to the identifier 'replace'
7
8(refer 'clojure.string :only '[replace])
9; => nil
10
11;; create references to all Vars from clojure.string
12;; except 'replace' and 'reverse'
13
14(refer 'clojure.string :exclude '[replace reverse])
15; => nil
16
17;; create references to all Vars from clojure.string
18;; except 'replace', and rename 'reverse' to 'backwards'
19
20(refer 'clojure.string
21 :exclude '[replace]
22 :rename '{reverse backwards})
23; => nil
;; create references to all Vars from clojure.string
(refer 'clojure.string)
; => nil
;; create a reference only to the identifier 'replace'
(refer 'clojure.string :only '[replace])
; => nil
;; create references to all Vars from clojure.string
;; except 'replace' and 'reverse'
(refer 'clojure.string :exclude '[replace reverse])
; => nil
;; create references to all Vars from clojure.string
;; except 'replace', and rename 'reverse' to 'backwards'
(refer 'clojure.string
:exclude '[replace]
:rename '{reverse backwards})
; => nil
See also:
Adding Clojure References, refer-clojure
A variant of the refer function is the refer-clojure macro, which can be treated
as syntactic sugar since it calls refer with the first argument value set to
'clojure.core.
refer-clojure is often used when we want to opt out of automatically creating
references to all functions and macros defined in the main Clojure library and
select only those we use in a given source file. Usually it will then be called
indirectly, through one of the clauses of the ns macro.
When might we need this? For example, when our library defines in its own namespace
functions or macros with the same names as in clojure.core. We then avoid warnings
about shadowing existing identifiers.
The macro returns nil.
Usage:
(refer-clojure & filters).
As a result of evaluating the code produced by the macro, references to Var objects
originating from clojure.core will be added in the current namespace. Subsequent,
optional arguments may express filters that will be used to refine the operations
performed.
In the case where a reference being created has the same symbolic name as an already existing mapping in the current namespace, it will be overwritten, and an appropriate warning will be sent to the standard diagnostic output.
In the current namespace, Var objects residing in clojure.core will be directly
associated with symbols. When a binding in the original namespace is removed (e.g.,
using ns-unmap), the current namespace will still contain the mapping of
the symbol to the Var object.
The available filters are:
:exclude symbol-sequence– symbols to skip,:only symbol-sequence– symbols to process exclusively,:rename symbol-map– symbols to rename.
These filters should consist of a name in the form of a keyword, followed
by a sequence of symbols specifying the filter values, which will be
symbolic names to include (:only) or skip (:exclude).
The sequences can be any collections (e.g., vectors) equipped with
a sequential access interface. If renaming was requested (:rename), then instead of
a sequence one should provide a map containing transformation pairs. In this
way, a given global Var can be referred to in the current namespace under a different
symbolic name.
refer-clojure macro
1;; remove bindings for apply and format
2;; from the current namespace
3
4(ns-unmap *ns* 'apply)
5(ns-unmap *ns* 'format)
6
7;; create references to all Vars from clojure.core
8;; except apply and format
9
10(refer-clojure :exclude '[apply format])
11; => nil
12
13;; no warning when defining apply
14;; in the current namespace
15
16(defn apply [] "application")
;; remove bindings for apply and format
;; from the current namespace
(ns-unmap *ns* 'apply)
(ns-unmap *ns* 'format)
;; create references to all Vars from clojure.core
;; except apply and format
(refer-clojure :exclude '[apply format])
; => nil
;; no warning when defining apply
;; in the current namespace
(defn apply [] "application")
Adding References to Java Classes, import
The import macro adds mappings from symbols to Java classes in the current
namespace.
The accepted arguments may be individual symbols – meaning that specific classes
placed in packages are to be imported. The argument may also be a list of symbols – in
that case the first one designates the package (e.g., java.util), and the subsequent
ones are names of classes from that package (e.g., Date).
As a result of the macro’s operation, symbolic names identical to the Java class names will be added to the current namespace, along with references to those classes associated with them.
The macro returns the object value of the last imported class, and in the case of a nonexistent package or class name, an exception is thrown.
Usage:
(import [& symbol…] & symbol-list…).
Where symbol-list is:
(package-symbol class-name-symbols).
import macro
1;; importing individual classes
2
3(import java.util.Date)
4
5;; importing selected classes from a package
6
7(import '(java.util Date Dictionary))
8
9;; using an object
10
11(Date.)
12; => #inst "2015-04-02T11:35:43.980-00:00"
;; importing individual classes
(import java.util.Date)
;; importing selected classes from a package
(import '(java.util Date Dictionary))
;; using an object
(Date.)
; => #inst "2015-04-02T11:35:43.980-00:00"
The import macro can also be used to import new data types created in Clojure
(e.g., using deftype or defrecord).
See also:
Interning Var Objects, intern
The function intern serves to intern objects of type Var, thereby creating global
Vars. It takes two arguments: a namespace name expressed as a
symbol value or namespace object, and a variable name expressed as a symbol value.
Using intern causes a mapping of the given symbol to an object of type Var to be
created in the specified namespace. If a Var object already exists under the given
name, it will not be replaced by a new one.
In the three-argument version, the function initializes the object with the given
value, meaning it sets inside the global Var a reference pointing to the indicated
in-memory object (the so-called root binding of the global Var). If the global Var
already exists, its reference will be updated without creating a new Var object.
The function returns the Var object identified by the symbol.
Note: The function intern replaces already existing references to Java classes in
the namespace with Var objects of the same names, even when no initial value has
been set.
Usage:
(intern namespace symbolic-name & initial-value).
intern function
1(intern 'user 'variable) ; => #'user/variable
2(intern 'user 'variable 5) ; => #'user/variable
3user/variable ; => 5
4variable ; => 5
5(intern (find-ns 'user) 'a 10) ; => #'user/a
(intern 'user 'variable) ; => #'user/variable
(intern 'user 'variable 5) ; => #'user/variable
user/variable ; => 5
variable ; => 5
(intern (find-ns 'user) 'a 10) ; => #'user/a
We can cause the interned global Var to be treated as private, meaning it will be
visible only in the namespace to which it was added. In practice this will mean that
only constructs from areas of the program where the current namespace is set to the
same one as the namespace of the defined variable can refer to such a variable using
its identifier. To mark a global Var as private, one should use the
so-called symbol metadata on the passed name argument. Specifically, this
involves the metadata designated by the key :private.
intern function to create private bindings
1(intern 'user '^:private variable) ; => #'user/variable
2(intern 'user '^{:private true} variable-2) ; => #'user/variable-2
3(intern 'user (with-meta 'variable-3 {:private true})) ; => #'user/variable-3
(intern 'user '^:private variable) ; => #'user/variable
(intern 'user '^{:private true} variable-2) ; => #'user/variable-2
(intern 'user (with-meta 'variable-3 {:private true})) ; => #'user/variable-3
The complete list of metadata that are relevant when interning a Var object is given
in the description of the special form def.
Var objects can be updated by calling the intern function again. Updating consists
of binding them to new values. If the variable already exists, its object in the
namespace will not be replaced by another one; its internal reference to a specific
value will simply be changed.
1(intern 'user 'xx 5) ; creation and binding to value 5
2
3(pprint #'xx) ; displaying the reference object
4; >> #<Var@7a0ad359: 5>
5
6(intern 'user 'xx 7) ; binding to value 7
7(pprint #'xx) ; displaying the reference object
8; >> #<Var@7a0ad359: 7>
(intern 'user 'xx 5) ; creation and binding to value 5
(pprint #'xx) ; displaying the reference object
; >> #<Var@7a0ad359: 5>
(intern 'user 'xx 7) ; binding to value 7
(pprint #'xx) ; displaying the reference object
; >> #<Var@7a0ad359: 7>
Note: The function intern can be used to change the root binding, which is shared
across threads, even when we are inside a construct that isolates the variable within
a thread (e.g., in a dynamic scope).
Adding Var Objects, def
The special form def works similarly to intern, but operates on the current
namespace and requires an unquoted symbol as the first argument. In this
position the symbol is a binding form, not an expression to resolve.
The def form takes one mandatory argument (the aforementioned symbol whose name is
to be associated with the created Var object) and two optional arguments: a
documentation string and an initial value for the global Var (which will be used for
updating if the variable already exists).
If a symbol with a qualified namespace is provided, it must be the current namespace –
otherwise an exception will be thrown. An exception will also occur when def is used
to update a mapping that identifies a Java class or is a reference to an object from
another namespace.
The special form returns a Var object identified by the symbol, which is identical to
the object placed in the namespace.
Usage:
(def symbol documentation-string? initial-value?).
def special form
1(def variable) ; => #'user/variable
2(def variable 5) ; => #'user/variable
3(def variable "Variable documentation" 5) ; => #'user/variable
4user/variable ; => 5
5variable ; => 5
6
7;; accessing the documentation - the doc function
8
9(doc variable)
10; >> user/variable
11; >> Variable documentation
12; => nil
(def variable) ; => #'user/variable
(def variable 5) ; => #'user/variable
(def variable "Variable documentation" 5) ; => #'user/variable
user/variable ; => 5
variable ; => 5
;; accessing the documentation - the doc function
(doc variable)
; >> user/variable
; >> Variable documentation
; => nil
In the case of def, one can also add metadata to the symbol indicating
that the mapping in the namespace should be private, that is, visible only to
constructs from the same namespace.
Metadata associated with the symbol will be copied to the Var object during its
creation.
def special form to create a private global Var
1(def ^:private variable) ; => #'user/variable
2(def ^{ :private true } variable 5) ; => #'user/variable
(def ^:private variable) ; => #'user/variable
(def ^{ :private true } variable 5) ; => #'user/variable
Below is a list of all metadata that are significant when using def:
| Key | Type | Meaning |
|---|---|---|
:private |
java.lang.Boolean |
Boolean flag indicating that the variable should be private |
:dynamic |
java.lang.Boolean |
Boolean flag indicating that the variable should be dynamic |
:doc string |
java.lang.String |
A text string documenting the variable's identity |
:tag object |
Class or Symbol |
A symbol constituting the name of a class or a Class object that
indicates the type of the Java object contained in the variable (unless it is
a function – then it will be its return value) |
:test function |
(implementing IFn) |
A zero-argument function invoked by Clojure's test facilities |
During the creation of a Var object, the following metadata will be automatically
placed in it:
| Key | Type | Meaning |
|---|---|---|
:file |
java.lang.String |
Source file name |
:line |
java.lang.Integer |
Source file line number |
:name |
clojure.lang.Symbol |
Variable name |
:ns |
clojure.lang.Namespace |
Namespace |
:macro |
java.lang.Boolean |
Flag indicating that the object refers to a macro |
:arglists |
A list of vectors | Parameter vectors, if the Var names a function or macro |
Global Vars can be updated by, among other means, evaluating the def special form
again. If the Var identified by the symbol already exists, its namespace mapping and
identity remain in place while its root binding is changed.
Var object
1(def xx 5) ; creation and binding to value 5
2(pprint #'xx) ; displaying the object
3; >> #<Var@4eee52c: 5>
4
5(def xx 7) ; binding to value 7
6(pprint #'xx) ; displaying the object
7; >> #<Var@4eee52c: 7>
(def xx 5) ; creation and binding to value 5
(pprint #'xx) ; displaying the object
; >> #<Var@4eee52c: 5>
(def xx 7) ; binding to value 7
(pprint #'xx) ; displaying the object
; >> #<Var@4eee52c: 7>
Note: The special form def can be used to change the root binding, which is shared
across threads, even when we are inside a construct that isolates the variable within
a thread (e.g., in a dynamic scope).
One-Time Adding of Vars, defonce
The defonce macro interns a Var in the current namespace. A qualified
symbol, if used, must name that same namespace. It works similarly to
def, but does not evaluate the initialization expression or replace the
root binding when the Var already has one.
The macro does not accept a separate documentation string because it has only two arguments: an unquoted symbol, which may carry metadata, and an initialization expression whose value becomes the Var’s root binding on the first initialization.
The macro returns a Var object if the root binding was set, or the value nil if
the binding already existed.
The defonce macro is useful for preserving an initialized root across repeated file
loads, for example during interactive development. It does not make the value a
language-level constant: other operations can still alter the Var’s root.
Note: Metadata supplied on the symbol is still applied when the root already exists.
Usage:
(defonce symbol expression).
defonce macro
1(defonce variable 5) ; => #'user/variable
2(defonce ^:example variable 1000) ; => nil
3user/variable ; => 5
4variable ; => 5
5
6(meta #'variable)
7; => { :ns #<Namespace user>, :name variable,
8; => :example true, :file "NO_SOURCE_PATH",
9; => :column 1, :line 1 }
(defonce variable 5) ; => #'user/variable
(defonce ^:example variable 1000) ; => nil
user/variable ; => 5
variable ; => 5
(meta #'variable)
; => { :ns #<Namespace user>, :name variable,
; => :example true, :file "NO_SOURCE_PATH",
; => :column 1, :line 1 }
Removing Mappings, ns-unmap
Thanks to the function ns-unmap, we can remove from a namespace mappings of symbols
to global Vars or Java classes. It takes two arguments. The first specifies
the namespace (using a symbol value or a namespace object), and the second
is the symbolically expressed name of the specific mapping to be removed.
The function returns nil, and when the given namespace does not exist, an exception
is thrown.
Usage:
(ns-unmap namespace symbolic-name).
ns-unmap function
1;; we create a global Var x
2
3(def x 5)
4; => #'user/x
5
6;; we retrieve its value
7
8x
9; => 5
10
11;; additionally we create a reference to the Var object of this variable
12
13(def y (var x))
14
15;; we remove the mapping
16
17(ns-unmap 'user 'x)
18; => nil
19
20;; we check whether the identifier x is still visible
21
22(resolve 'x)
23; => nil
24
25;; we check whether the Var object itself exists,
26;; although it is no longer bound to the symbol x
27
28(deref y)
29; => 5
;; we create a global Var x
(def x 5)
; => #'user/x
;; we retrieve its value
x
; => 5
;; additionally we create a reference to the Var object of this variable
(def y (var x))
;; we remove the mapping
(ns-unmap 'user 'x)
; => nil
;; we check whether the identifier x is still visible
(resolve 'x)
; => nil
;; we check whether the Var object itself exists,
;; although it is no longer bound to the symbol x
(deref y)
; => 5
In the example above, removing the mapping does not destroy the Var; the separate
reference y still reaches it. The Var also retains its :name and :ns metadata,
but ordinary namespace resolution can no longer find it through the removed mapping.
Adding Aliases, alias
The alias mechanism allows referring to different namespaces using alternative identifiers placed in the current namespace.
The function alias adds alternative names for other namespaces to the
current namespace. It takes two arguments: the first should be a symbol value, and the
second a namespace object or its name expressed as a symbol value. The
first argument is the alias name, and the second is the namespace to which a reference
is to be created.
The function returns nil, and in the case of specifying a nonexistent namespace, an
exception is thrown.
Usage:
(alias symbolic-name namespace).
alias function
(alias 'st 'clojure.string)
(st/reverse "abcdef")
; => "fedcba"
See also:
Removing Aliases, unalias
The function ns-unalias removes aliases added using alias. It takes two
arguments. The first should specify the namespace using a symbol value or
a namespace object, and the second should be the alias name expressed symbolically.
The function always returns nil, regardless of whether the given alias existed or
whether its removal was not possible because it was not in fact an alias. When the
given namespace does not exist, an exception is thrown.
Usage:
(ns-unalias namespace symbolic-name).
ns-unalias function
(ns-unalias 'user 'st)
; => nil
Reading Contents
Reading the name, ns-name
The function ns-name takes a namespace object (or a symbol representing its name)
as an argument, and returns a symbol identifying the namespace’s name. If the
namespace does not exist, an exception is thrown.
Usage:
(ns-name namespace).
ns-name function
(ns-name 'user) ; => user
(ns-name (find-ns 'user)) ; => user
Reading aliases, ns-aliases
The function ns-aliases takes a single argument, which should be a symbol in
literal form specifying the name of a namespace or an object of that namespace, and
returns a map containing defined aliases, that is, mappings of other
symbolic identifiers to namespace objects. If the namespace does not exist, an
exception will be thrown.
Usage:
(ns-aliases namespace).
ns-aliases function
(ns-name (get (ns-aliases 'clojure.core) 'jio))
; => clojure.java.io
Reading Var mappings, ns-interns
The function ns-interns takes a single argument, which should be a symbol in
literal form specifying the name of a namespace or an object of that namespace, and
returns a map containing mappings of symbolic identifiers to global
variables (objects of type Var). If the namespace does not exist, an exception
will be thrown.
Usage:
(ns-interns namespace).
ns-interns function
(identical? (get (ns-interns 'clojure.core) 'map)
#'clojure.core/map)
; => true
Reading Var references, ns-refers
The function ns-refers takes a single argument, which should be a symbol in
literal form specifying the name of a namespace or an object of that namespace, and
returns a map containing mappings of symbolic identifiers to objects of
type Var that were referred into the specified namespace (e.g., using
refer).
Usage:
(ns-refers namespace).
ns-refers function
(identical? (get (ns-refers 'user) 'map)
#'clojure.core/map)
; => true
It is worth knowing that the original binding of a symbol to a Var in another
namespace may be removed (e.g., using the ns-unmap function). In such
a case, the binding reflected in the current namespace will not disappear, because
the symbol will be mapped directly to the Var object, rather than to an entry
in the internal map of another namespace. The downside of such a situation may,
however, be a certain inconsistency in the metadata of the target object with the
actual state: the metadata stored in the Var under the key :ns will point to
the original namespace, in which we will no longer find the binding.
Reading class references, ns-imports
The function ns-imports takes a single argument, which should be a symbol in
literal form specifying the name of a namespace or an object of that namespace, and
returns a map containing mappings of symbolic identifiers to references
pointing to Java classes. If the namespace does not exist, an exception will be
thrown.
Usage:
(ns-imports namespace).
ns-imports function
(identical? (get (ns-imports 'clojure.core) 'String)
java.lang.String)
; => true
Reading all mappings, ns-map
The function ns-map takes a single argument, which should be a symbol in literal form
specifying the name of a namespace or an object of that namespace, and returns a map
containing all mappings of symbolic identifiers to objects (Vars and Java
classes). If the namespace does not exist, an exception will be
generated.
Usage:
(ns-map namespace).
ns-map function
(every? #(or (var? %) (class? %))
(vals (ns-map 'clojure.core)))
; => true
Reading public mappings, ns-publics
The function ns-publics takes a single argument, which should be a symbol
in literal form specifying the name of a namespace or an object of that namespace,
and returns a map containing its public interned Vars. Referred Vars and
imported classes are not included. If the namespace does not exist, an exception will
be generated.
Usage:
(ns-publics namespace).
ns-publics function
(identical? (get (ns-publics 'clojure.core) 'map)
#'clojure.core/map)
; => true
Library Handling
A library, or more precisely a software library, is a collection of resources placed in files that can be utilized by software to enrich the available functions. A library may contain data, subroutines (e.g., macros or functions), and even definitions of new data types. Thanks to libraries, it is possible to reuse already-implemented methods for solving problems.
Depending on the programming language, software libraries may consist exclusively of source code or may appear in compiled versions supplemented with source files containing declarations, thanks to which the compiler can link subroutine calls with the corresponding implementations in machine language or bytecode.
On the JVM, a library is commonly distributed as a JAR. A single JAR may contain Clojure source files, JVM class files produced from Clojure or Java, and ordinary resources. What gets loaded depends on the namespace and classpath entries being requested; a JAR is not restricted to one kind of content.
Loading Libraries
Loading libraries and placing the needed mappings in the current namespace uses the
operations presented earlier, such as refer and import. The
require function and the declarative clauses of the ns macro provide the usual
interface for this work.
In Clojure, the files of a given library should be located in a directory placed on the classpath, and by convention, its name will be expressed as a symbol in literal form (when passing it to various macros or functions).
Standalone loading, load
The function load is responsible for loading libraries. It accepts zero or more
arguments, which should be file system paths expressed as character strings.
Usage:
(load & path…).
For each given relative path (not beginning with a path name separator), the library file will be looked for in the root directory of the current namespace. The root directory is obtained by:
- taking the name of the current namespace;
- prepending a slash character (
/); - replacing all hyphens (
-) with underscore characters (_); - replacing all dots (
.) with slash characters (/); - extracting the fragment from the beginning to the last occurrence of a slash;
- appending a slash character at the end;
- appending the path given as the argument at the end.
For each absolute path (beginning with a path name separator), a search will be performed across all locations that are combinations of successive paths placed on the classpath.
Examples of root directories depending on the namespace name and the given path:
-
current namespace
user:(load "test"):/test,(load "one/two"):/one/two,
-
current namespace
clojure.core:(load "test"):/clojure/test,(load "one/two"):/clojure/one/two,(load "string"):/clojure/string.
Regardless of whether a relative or absolute path was given, the last element of
the given path will be treated as the file name to load, and the text string with
the .clj extension will be appended to it.
We can verify how the names are constructed by setting the dynamic variable
*clojure.core/loading-verbosely* to a value other than false and other than nil
within the dynamic scope of the binding macro.
load function
1;; loading a project file
2;; src/project/core.clj
3
4(load "project/core")
5; => nil
6
7;; loading the root file
8;; of the clojure.string library
9
10(load "/clojure/string")
11; => nil
;; loading a project file
;; src/project/core.clj
(load "project/core")
; => nil
;; loading the root file
;; of the clojure.string library
(load "/clojure/string")
; => nil
The require Function
The require function loads Clojure libraries. Because its library specifications are
data, direct calls normally quote them. Each argument should
be one of several clauses:
- a library spec (library specification),
- a prefix list,
- a modifier flag.
A library spec is either a symbolically expressed library name, or a vector containing the name and additional parameters. The names of these parameters should be expressed as keywords and grouped in a sequential collection. Thanks to the library spec parameters, we can decide what happens right after the library is loaded into memory.
Possible parameters are:
-
:as symbolic-name– uses thealiasfunction and creates a reference to the loaded library under the given name in the current namespace; -
:refer symbol-vector-or-:all– uses thereferfunction to create mappings in the current namespace for selected public Vars, or for all public Vars when the value is:all; -
:as-alias symbolic-name– creates only a namespace alias, without loading the target library. This is useful when qualified names are used as data.
A prefix list makes it possible to load libraries whose names share the same beginning. This saves our keyboards and our fingers. Instead of the given common prefix, one creates a list of library specs. An important condition is that names on this list may no longer contain dots, i.e., they must be the last elements of the path (and name).
Modifier flags influence the behavior of the function. These are keywords:
-
:reload– forces re-loading of libraries into memory, even if they have already been loaded; -
:reload-all– works like:reload, but affects all dependent libraries loaded by the one being read (if they make use ofuseorrequire); -
:verbose– causes diagnostic information about loading and creating references to be printed.
The function locates and loads the library’s root resource, whose path is derived from the namespace name. Loading a Clojure namespace does not automatically create a corresponding Java package. The resource path is constructed according to the following scheme:
- dots are replaced with path name separators (e.g.,
a.bbecomesa/b); - the last part of the name is treated as the file name (e.g.,
b.clj); - the remaining part of the name is treated as the root directory name (e.g.,
a); - the relative path along with the file name is appended to successive classpath entries until the library’s root file is found.
The root file should define the namespace of the entire library.
If the library has already been loaded into memory previously, it will not be loaded again.
Usage:
(require & spec… & prefix-list… & flag…).
require function
1;; will load the library and create the clojure.string namespace
2
3(require 'clojure.string)
4; => nil
5
6;; we can specify multiple libraries
7
8(require 'clojure.string 'clojure.test 'clojure.set)
9
10;; we can specify multiple libraries with a common prefix
11
12(require '[clojure string test set])
13
14;; will load the library and create the clojure.string namespace
15;; even if it was already loaded
16
17(require 'clojure.string :reload :verbose)
18; => (clojure.core/load "/clojure/string")
19; => nil
20
21;; will load the library if it has not been loaded yet
22;; and create an alias in the current namespace so that
23;; the clojure.string namespace is visible as st
24
25(require '[clojure.string :as st] :verbose)
26; => (clojure.core/in-ns 'user)
27; => (clojure.core/alias 'st 'clojure.string)
28; => nil
;; will load the library and create the clojure.string namespace
(require 'clojure.string)
; => nil
;; we can specify multiple libraries
(require 'clojure.string 'clojure.test 'clojure.set)
;; we can specify multiple libraries with a common prefix
(require '[clojure string test set])
;; will load the library and create the clojure.string namespace
;; even if it was already loaded
(require 'clojure.string :reload :verbose)
; => (clojure.core/load "/clojure/string")
; => nil
;; will load the library if it has not been loaded yet
;; and create an alias in the current namespace so that
;; the clojure.string namespace is visible as st
(require '[clojure.string :as st] :verbose)
; => (clojure.core/in-ns 'user)
; => (clojure.core/alias 'st 'clojure.string)
; => nil
The use Function
The use function combines loading with referring public Vars into the current
namespace. It accepts the same legacy style of quoted library specifications as
require, but its broad imports make dependencies and name collisions less
obvious.
The use function can accept additional parameters in library specs:
:exclude symbol-sequence– symbols to be excluded,:only symbol-sequence– symbols to be exclusively processed,:rename symbol-map– symbols to be renamed.
Usage:
(use & spec… & prefix-list… & flag…).
Prefer the ns macro with :require, aliases, and an explicit :refer vector
instead of use.
The ns macro
The ns macro groups namespace declaration and dependency operations in one place,
normally at the beginning of a source file.
The macro can set the current namespace (creating it if necessary), load required libraries, establish referrals and aliases, import Java classes, and request JVM class generation during AOT compilation.
The macro takes the namespace name and an optional set of clauses describing required
libraries, imports, and related operations. These clauses are declarative syntax
interpreted by ns; their entries are not first evaluated as ordinary call
arguments, which is why library names do not need quoting there.
Optionally, after the namespace name, one may provide a documentation string (e.g., describing the source file), as well as an attribute map.
Usage:
(ns namespace-name docstring? attr-map? & clause…).
Reference clauses:
(:require …)– invokesrequire,(:use …)– invokesuse,(:import …)– invokesimport,(:load …)– invokesload,(:gen-class …)– invokesgen-class,(:refer-clojure …)– invokesrefer-clojure.
In the case of gen-class, the arguments passed to the invocation by default are:
:name namespace-name,:impl-ns namespace-name,:main true.
The :gen-class clause emits the requested named class during AOT compilation; it has
no effect when the source is merely loaded. AOT compilation without :gen-class
still produces the implementation classes needed for the namespace, and their exact
set is an implementation detail.
ns macro
1(ns io.randomseed.examples
2 (:refer-clojure :exclude [printf])
3 (:require [clojure.set :as set ]
4 [clojure.string :as string ]
5 [clojure.repl :refer [doc dir] ]
6 [io.randomseed.resources.file :as files ])
7 (:use [io.randomseed.handy :only [a-func other]])
8 (:import [java.util Date Random ]))
(ns io.randomseed.examples
(:refer-clojure :exclude [printf])
(:require [clojure.set :as set ]
[clojure.string :as string ]
[clojure.repl :refer [doc dir] ]
[io.randomseed.resources.file :as files ])
(:use [io.randomseed.handy :only [a-func other]])
(:import [java.util Date Random ]))
In the example above, we can see that the namespace io.randomseed.examples is being
created, and right after that, references to global variables from the language’s
standard library are loaded, but excluding the object identified by the symbol
printf.
Next, within the namespace io.randomseed.examples, aliases are created for the
namespaces clojure.set, clojure.string, and io.randomseed.resources.file to
make them easier to specify. In the same section, references to global variables from
the clojure.repl namespace (including doc and dir) are also created so that
they can be invoked without specifying the namespace.
The :use clause works similarly to :require with the :refer parameter, i.e., in
the given namespace (here io.randomseed.handy) objects are located (here with the
names a-func and other) and in the namespace being handled by the macro (here
io.randomseed.examples) references to them are created. It is recommended to use
:require (with the :refer parameter) instead of :use.
The last clause (:import) creates references to Java classes (Date and Random)
from the java.util package.
Handling Bindings
Bindings and references answer different questions. A name-resolution context tells us what a symbol denotes; a reference object tells us which state is current; and a dynamic Var binding tells us which temporary value applies in the current execution context. Keeping these layers separate makes shadowing and mutation much easier to reason about.
In practice, handling them consists of:
- giving names to values using binding forms of symbols,
- creating references to values using so-called reference types,
- reading binding values using:
- symbol forms (in the case of identifiers);
- dereference forms (in the case of reference types).
Reading binding values is a task for the language mechanisms (it is enough to use an unquoted symbol in the source code), while in the case of reference types it is a task for the programmer, greatly aided by ready-made functions and reader macros. Below we will therefore focus on creating bindings depending on their types and the constructs used for this purpose.
Lexical binding forms associate symbols with values for a lexical
region. Those associations are immutable; a nested lexical binding can
shadow an outer one. Namespace mappings are managed separately, and a
dynamic binding does not create a new symbol mapping—it selects a temporary current
value for an existing dynamic Var.
In the case of reference types, we can update current values that instances of these types refer to, using appropriate functions. This way we can create stable identities that will refer to changing states.
Types of Bindings
For this chapter it is useful to distinguish three layers:
- name bindings and mappings, which associate symbols with lexical values, Vars, or Java classes;
- reference state, in which a stable reference object exposes a current value;
- dynamic Var bindings, which temporarily override a dynamic Var’s root binding for a dynamic execution extent.
A dynamic Var is still an ordinary namespace-mapped Var object. The additional
:dynamic metadata permits stack-disciplined thread bindings; it does not introduce
a different namespace mapping mechanism.
Symbol Bindings
Symbols participate in two distinct name-resolution contexts:
-
in namespaces:
-
in lexical bindings:
- to local values (forms
let,loopand similar); - to local Var objects (form
with-local-vars); - to function arguments in their definitions
(so-called parameter bindings – forms
fn,defn);
- to local values (forms
Destructuring is syntax for creating several lexical or parameter bindings from one input value; it is not a third storage mechanism.
Semantically correct symbol bindings in certain contexts will also be called binding forms of symbols.
Structural Bindings
Structural bindings are lexical or parameter bindings in which destructuring of an associative structure (e.g., a map) or a sequential one (e.g., a vector) takes place, in order to bind multiple symbols to values at once.
Destructuring, which will be discussed later, can be imagined as a way of creating bindings using two similarly arranged structures. On the left side we place a structure containing unquoted symbols, and on the right a structure isomorphic to it with initializing values. Symbols placed in the left structure will be bound to values from the right structure depending on position (in the case of sequential collections, e.g., vectors) or keys (in the case of maps).
Reference Object Bindings
Reference objects provide stable identities whose observable state follows an explicit update or realization contract. Mutable reference types include:
- global Vars, local
and dynamic (type
Var), - Atoms (type
Atom), - Agents (type
Agent), - Refs (type
Ref), - Volatiles (type
Volatile).
Futures, Promises, and Delays also provide stable identities and dereferenceable results, but they follow one-shot computation or delivery contracts rather than supporting arbitrary repeated state updates.
Dynamic Bindings
Dynamic bindings serve to temporarily shadow the values
of global Vars that have the :dynamic flag set in their
metadata. Such Vars are called dynamic Vars. The
binding macro establishes a stack-disciplined thread binding for its body
and for calls made within that dynamic extent. The namespace still contains the same
Var mapping.
Binding Scopes
Binding scope is the area of a program in which a given binding can be used.
Besides the scope of a binding we can also speak about the visibility of the identified value, that is, the area in which it is possible to refer to it. Visibility of a value depends on the binding scope, but it can also be additionally controlled using namespaces.
Visibility can be smaller than scope if in a given context the same symbol is used to denote more than one binding. We then speak of shadowing.
In Clojure we encounter lexical scope, namespace-level availability, and the dynamic extent of thread bindings.
Namespace-Level Availability
Mappings to global Vars and Java classes remain available for as long as their namespace and mappings remain registered. Access still depends on qualification, aliases, referrals, privacy, and whether the namespace has been loaded; “global” does not mean that every unqualified symbol can see them.
Namespace mappings let programs give stable names to Vars. A Var’s root may change over time while the Var identity and its mapping stay in place, unless code explicitly unmaps or replaces that mapping.
An example of namespace-level naming is function names. Symbolic
identifiers are bound in namespaces to reference objects of type Var, which in
turn contain references to function type objects. It is precisely thanks to
namespaces that controlling visibility in this scope is possible.
1(ns ours) ; switch namespace
2(def x 5) ; global Var x (root binding with value 5)
3(defn read-x [] x) ; function that returns the value of global Var x
4(read-x) ; function call
5; => 5 ; result of the call
6
7(ns other) ; switch namespace
8(read-x) ; attempt to call the function
9; >> Unable to resolve symbol: read-x in this context
10
11(ours/read-x) ; calling the function with a namespace-qualified symbol
12; => 5
(ns ours) ; switch namespace
(def x 5) ; global Var x (root binding with value 5)
(defn read-x [] x) ; function that returns the value of global Var x
(read-x) ; function call
; => 5 ; result of the call
(ns other) ; switch namespace
(read-x) ; attempt to call the function
; >> Unable to resolve symbol: read-x in this context
(ours/read-x) ; calling the function with a namespace-qualified symbol
; => 5
Let us note that after switching the current namespace to other we lost
visibility of the value bound to the global Var read-x naming a function. The
binding did not disappear, which is why by using a symbol form
with a qualified namespace we can still make use of it.
Lexical Scope
We encounter lexical scope in the case of lexical bindings. The ability to use bindings covered by this scope depends on the placement of the symbols identifying them in the source code.
Lexical scope is used in many programming languages. We can then speak, for example, of local lexical scope (within a function body or a certain code block).
In Clojure, lexical scope:
- is created explicitly using the special form
letor similar ones; - is created automatically for function and macro parameters.
The let Form and the Binding Vector
Using the special form let, we can create lexical bindings whose scope
will be limited to the S-expressions given as its arguments.
The let form is very commonly used in Clojure and in other Lisp
dialects. One could say that alongside forms creating functions or
lists it is one of the fundamental constructs of the language. Thanks to
it we can write readable, declarative code and give values symbolic labels in
selected areas of the program.
Usage:
(let binding-vector & expression...),
where binding-vector is:
[binding-form init-expression ...].
The first argument that should be passed to the let construct is the binding
vector. It is a vector S-expression that should consist
of so-called binding pairs. The first elements of these pairs should be
binding forms, and the second ones so-called initialization expressions,
which will be evaluated to values.
Binding forms in the binding vector of the let form can be expressed using:
-
individual symbols, e.g.,
aorbfor the vector[a 5 b 10]; -
vector binding expressions, e.g.,
[a b]for the vector[[a b] [5 10]]; -
binding maps, e.g.,
{a :a b :b}for the vector[{a :a b :b} {:a 5 :b 10}].
Symbols should express binding forms, and thus appear in unquoted form, while maps or vectors are used in the case of so-called destructuring, which will be discussed later and can create abstract structural bindings. They find application when there is a need to bind symbols to values of specific elements originating from multi-element structures.
let special form
1(let [a 1 ; binding symbol a to value 1
2 b (inc a) ; binding symbol b to value a+1
3 c 3] ; binding symbol c to value 3
4 (+ a b c)) ; bindings visible only in the let expression
5; => 6
(let [a 1 ; binding symbol a to value 1
b (inc a) ; binding symbol b to value a+1
c 3] ; binding symbol c to value 3
(+ a b c)) ; bindings visible only in the let expression
; => 6
Values assigned to binding forms can be represented by any S-expressions that will become valid forms. We call them initialization expressions in this context. In initialization expressions we can refer to symbols that were bound to values at earlier positions of the binding vector.
The subsequent, optional arguments of let are S-expressions to evaluate, in
which we can use the previously bound symbols. When a given symbol is provided,
its symbol form will be evaluated to a value.
It is worth noting that let introduces local lexical bindings, not Var
objects. The compiler represents and optimizes those locals according to the target
runtime; their JVM stack, local-slot, or heap representation is not part of Clojure’s
language-level contract.
We can shadow the values of lexical bindings by creating new ones that use the same symbolic names:
1(let [a 1 ; binding symbol a to value 1
2 b (inc a) ; binding symbol b to the value bound to a + 1
3 a b] ; binding symbol a to the value bound to b
4 a)
5; => 2
(let [a 1 ; binding symbol a to value 1
b (inc a) ; binding symbol b to the value bound to a + 1
a b] ; binding symbol a to the value bound to b
a)
; => 2
The let form evaluates to the value of the last evaluated S-expression or to
nil if no expression was provided.
The lexical scope of bindings created in the binding vector of the let form
is limited to the initialization expressions of its vector and the S-expressions
given as its arguments. The scope of each binding in the vector begins from the
place of its creation – in the initialization expressions of the vector we can
use bindings created at earlier positions.
Conditional Binding, if-let
The if-let macro works similarly to the let special form, internally
making use of the if form. It creates one lexical binding
visible in S-expressions that will be evaluated depending on whether the value
from the initialization expression represents logical truth or falsehood.
The first argument of the if-let macro is a binding vector, the second should
be an S-expression that will be evaluated if the value of the initialization
expression in the vector is truthy (is not equal to false or nil). After it
an optional third argument may appear, which will be evaluated if the value of
the second turns out to be falsy (equal to false or nil). It is worth
remembering that in this expression we cannot use the binding, because it will
not be created.
The return value is the value of the last evaluated expression or nil if no
expression was evaluated (because, e.g., the truth condition was not met and no
additional expression to evaluate was provided).
Usage:
(if-let binding-vector truth-expression falsy-expression?)
if-let macro
1(if-let [a 1] a) ; => 1
2(if-let [a 0] a) ; => 0
3(if-let [a false] a) ; => nil
4(if-let [a nil] a) ; => nil
5(if-let [a nil] a "none") ; => "none"
(if-let [a 1] a) ; => 1
(if-let [a 0] a) ; => 0
(if-let [a false] a) ; => nil
(if-let [a nil] a) ; => nil
(if-let [a nil] a "none") ; => "none"
The lexical scope of the binding created in the binding vector of the if-let
form is limited to the S-expressions given as its arguments.
Function Binding, letfn
The letfn macro is a version of the let special form that allows
defining functions and creating their lexical bindings with symbols in
such a way that they become visible in all initialization expressions of
a given binding vector (even in those placed earlier).
In simple cases we can use let to bind a symbol to an anonymous
function, and then call that function:
(let [x (fn [a] (+ 2 a))]
(x 2))
; => 4
We can also call functions in the binding vector, during binding creation, and thus treat returned values as initialization expressions or their components:
1(let [x (fn [a] (+ 2 a)) ; function x
2 y (fn [a] (+ 3 (x a)))] ; function y uses function x
3 (y 2)) ; calling function y
4
5; => 7
(let [x (fn [a] (+ 2 a)) ; function x
y (fn [a] (+ 3 (x a)))] ; function y uses function x
(y 2)) ; calling function y
; => 7
Let us see, however, what happens when in the binding vector we refer to a function earlier than the binding of it to a symbol took place:
1(let [y (fn [a] (+ 3 (x a))) ; function y uses function x
2 x (fn [a] (+ 2 a))] ; function x
3 (y 2)) ; calling function y
4
5; >> java.lang.RuntimeException:
6; >> Unable to resolve symbol: x in this context
(let [y (fn [a] (+ 3 (x a))) ; function y uses function x
x (fn [a] (+ 2 a))] ; function x
(y 2)) ; calling function y
; >> java.lang.RuntimeException:
; >> Unable to resolve symbol: x in this context
We can see that this is not possible, because the expressions of the binding
vector are processed in a specific order. However, there are certain application
domains where we need to refer to a function object that will only be defined
later (e.g., in so-called mutual recursion). In such cases
letfn comes to the rescue.
Usage:
(letfn function-spec-vector & expression...);
where function-spec-vector is:
[(name param-vector expression...)],[(name (param-vector expression...)+)].
The second variant of the function specification vector is used to create so-called multi-arity functions, which are discussed in the chapter dedicated to functions.
letfn macro
(letfn [(y [a] (+ 3 (x a)))
(x [a] (+ 2 a))]
(y 2))
; => 7
The lexical scope of bindings created in the binding vector of the letfn form
is limited to the initialization expressions of its vector and the S-expressions
given as its arguments. The scope of each binding in the vector encompasses the
entire vector – in the initialization expressions of the vector we can use any
binding placed in it regardless of the order of creation.
See also:
- “Functions”, chapter III.
Conditional Binding, when-let
The when-let macro is a version of the let special form that internally
makes use of the when macro. It creates one lexical binding
visible in expressions that will be evaluated provided that the value from
the initialization expression represents logical truth (is not equal to false
or nil).
The first argument of when-let should be a binding vector
containing exactly one binding pair, and each subsequent one will be treated
as an expression to be evaluated in which one can use the symbol form referring
to the value bound in the vector.
The macro returns the value of the last evaluated expression or nil if no
evaluation took place because the truth condition was not met.
Usage:
(when-let binding-vector & expression...).
when-let macro
1(when-let [a 0] (str "got " a)) ; => "got 0"
2(when-let [a 1] (str "got " a)) ; => "got 1"
3(when-let [a nil] (str "got " a)) ; => nil
4(when-let [a false] (str "got " a)) ; => nil
(when-let [a 0] (str "got " a)) ; => "got 0"
(when-let [a 1] (str "got " a)) ; => "got 1"
(when-let [a nil] (str "got " a)) ; => nil
(when-let [a false] (str "got " a)) ; => nil
The lexical scope of the binding created in the binding vector of the
when-let form is limited to the S-expressions given as its arguments.
Binding of the 1st N-E, when-first
The when-first macro is a version of the when-let macro. It creates
a lexical binding visible in expressions that will be evaluated provided that
the value from the initialization expression is a structure that can be
converted to a non-empty sequence.
The first argument of when-first should be a binding vector,
containing exactly one binding pair, and each subsequent one will be treated
as an expression to be evaluated in which one can use the symbol form referring
to the value bound in the vector. The first element represented by the
initialization expression will be bound.
The macro returns the value of the last evaluated expression or nil if no
evaluation took place because the truth condition was not met.
Usage:
(when-first binding-vector & expression...).
when-first macro
1(when-first [a [0 1 2]] (str "got " a)) ; => "got 0"
2(when-first [a [false 2 3]] (str "got " a)) ; => "got false"
3(when-first [a [nil 2 3]] (str "got " a)) ; => "got "
4(when-first [a "123"] (str "got " a)) ; => "got 1"
5(when-first [a nil] (str "got " a)) ; => nil
6(when-first [a []] (str "got " a)) ; => nil
7(when-first [a ""] (str "got " a)) ; => nil
(when-first [a [0 1 2]] (str "got " a)) ; => "got 0"
(when-first [a [false 2 3]] (str "got " a)) ; => "got false"
(when-first [a [nil 2 3]] (str "got " a)) ; => "got "
(when-first [a "123"] (str "got " a)) ; => "got 1"
(when-first [a nil] (str "got " a)) ; => nil
(when-first [a []] (str "got " a)) ; => nil
(when-first [a ""] (str "got " a)) ; => nil
Note: The when-first macro calls the seq function on the value of
the initialization expression (the second element of the binding pair) and errors
may occur if such an operation is not possible (e.g., an integer or a boolean
value was provided).
The lexical scope of the binding created in the binding vector of the
when-first form is limited to the S-expressions given as its arguments.
Binding of Non-Nil Values, when-some
The when-some macro is a version of the let special form that internally
makes use of the when macro. It creates one lexical binding
visible in expressions that will be evaluated provided that the value from
the initialization expression is different from nil.
The first argument of when-some should be a binding vector,
containing exactly one binding pair, and each subsequent one will be treated
as an expression to be evaluated in which one can use the symbol form referring
to the value bound in the vector.
The macro returns the value of the last evaluated expression or nil if no
evaluation took place because the condition was not met.
Usage:
(when-some binding-vector & expression...).
when-some macro
1(when-some [a 0] (str "got " a)) ; => "got 0"
2(when-some [a 1] (str "got " a)) ; => "got 1"
3(when-some [a false] (str "got " a)) ; => "got false"
4(when-some [a nil] (str "got " a)) ; => nil
(when-some [a 0] (str "got " a)) ; => "got 0"
(when-some [a 1] (str "got " a)) ; => "got 1"
(when-some [a false] (str "got " a)) ; => "got false"
(when-some [a nil] (str "got " a)) ; => nil
The lexical scope of the binding created in the binding vector of the
when-some form is limited to the S-expressions given as its arguments.
Binding of Non-Nil Values, if-some
The if-some macro is a version of the let special form that internally makes
use of the if special form. It creates one lexical binding
visible in an expression that will be evaluated provided that the value from
the initialization expression is different from nil. Optionally, a second
expression can also be provided, which will be evaluated otherwise.
The first argument of if-some should be a binding vector,
containing exactly one binding pair, and the next (also mandatory) one will
be treated as an expression to be evaluated in which one can use the symbol form
referring to the value bound in the vector, if the initialization expression does
not have the value nil. The optional, third argument should contain a second
expression that will be executed when the value of the initialization expression
is nil. It is worth remembering that the binding will not be visible in it,
because it will not be created.
The macro returns the value of the last evaluated expression or nil if no
evaluation took place because the condition was not met.
Usage:
(if-some binding-vector non-nil-expression & nil-expression).
if-some macro
1(if-some [a 0] (str "got " a)) ; => "got 0"
2(if-some [a 1] (str "got " a)) ; => "got 1"
3(if-some [a false] (str "got " a)) ; => "got false"
4(if-some [a nil] (str "got " a)) ; => nil
5(if-some [a nil] (str "got " a) "none") ; => "none"
(if-some [a 0] (str "got " a)) ; => "got 0"
(if-some [a 1] (str "got " a)) ; => "got 1"
(if-some [a false] (str "got " a)) ; => "got false"
(if-some [a nil] (str "got " a)) ; => nil
(if-some [a nil] (str "got " a) "none") ; => "none"
The lexical scope of the binding created in the binding vector of the
if-some form is limited to the S-expressions given as its arguments.
Binding in a Loop, loop and recur
The loop special form works similarly to let, but allows
recursive execution of a program fragment. It accepts one
mandatory argument, which should be a binding vector and zero
or more arguments that are expressions in which one can use the lexical bindings
created in the vector. The return value is the value of the last evaluated
expression.
Bindings used in expressions inside loop can be updated in the recur
call. Arguments passed to recur will become the new binding values at their
corresponding positions in the vector during the next recursive invocation of
the expressions from loop. This makes so-called tail recursion
possible, which does not exhaust stack memory resources.
Usage:
(loop binding-vector & expression...).
loop special form
1(loop [x 1] ; loop and lexical binding
2 (when (< x 10) ; recursion termination condition
3 (println x) ; display; binding visible only in the loop
4 (recur (inc x)))) ; change binding of x and jump to the beginning
(loop [x 1] ; loop and lexical binding
(when (< x 10) ; recursion termination condition
(println x) ; display; binding visible only in the loop
(recur (inc x)))) ; change binding of x and jump to the beginning
The lexical scope of bindings created in the binding vector of the loop form
is limited to the initialization expressions of its vector (in the order of their
occurrence) and the S-expressions given as its arguments. The scope of each
binding in the vector begins from the place of its creation – in the
initialization expressions of the vector we can use bindings created at earlier
positions.
See also:
- “Starting Point”, chapter XII.
Parameter Binding
We will also encounter lexical scope when we define a function that accepts some arguments. We then speak of parameter bindings, that is, creating binding forms of symbols in parameter vectors of functions.
1(defn add [a b] ; definition of function add; parameters a and b
2 (+ a b)) ; visible only in the function body (within the S-expression)
3
4(fn [a b] ; definition of an anonymous function; parameters a and b
5 (+ a b)) ; visible only in the function body (within the S-expression)
(defn add [a b] ; definition of function add; parameters a and b
(+ a b)) ; visible only in the function body (within the S-expression)
(fn [a b] ; definition of an anonymous function; parameters a and b
(+ a b)) ; visible only in the function body (within the S-expression)
The lexical scope of parameter bindings to values passed as arguments during calls is limited to the function body.
Local Vars, with-local-vars
As an exception, we can give Var type objects local lexical scope. We will use
this kind of construct when we want to express some problem imperatively and
consequently there is a need to use an equivalent of local variables. The
with-local-vars macro serves this purpose and is discussed in more detail in
the chapter dedicated to Var type objects and variables.
Usage:
(with-local-vars binding-vector expression)
with-local-vars macro
(with-local-vars [a 1] @a)
; => 1
The lexical scope of the symbols introduced by with-local-vars is limited to
its body. Unlike let, its initialization expressions must not depend on values
introduced earlier in the same vector: the local Vars do not yet have those
thread-bound values while the initializers are evaluated. Dereferencing such a local
Var yields an internal Var$Unbound marker rather than the earlier initializer’s
value. Compute dependent values before entering with-local-vars, or inside its
body.
See also:
- “Local Vars”, chapter VII.
I/O Binding, with-open
The with-open macro creates lexical bindings similarly to let, then
guarantees a call to close() for every initialized resource when control leaves the
body. Resources are closed in reverse order, including when evaluation throws.
Usage:
(with-open binding-vector & expression...),
where binding-vector is:
[symbol-binding-form init-expression ...].
The first argument that should be passed to the with-open construct is the
binding vector. It is a vector S-expression that
should consist of so-called binding pairs.
The first elements of these pairs should be symbol binding forms (and not, as in the case of let, arbitrary binding forms), and the
second ones so-called initialization expressions, which will be evaluated to
closeable resource objects.
with-open macro
(with-open [reader (clojure.java.io/reader "file.txt")]
(doall (line-seq reader)))
In initialization expressions we can refer to symbols that were bound to values at earlier positions of the binding vector.
The subsequent, optional arguments of with-open are S-expressions to
evaluate, in which we can use the previously bound symbols. When a given symbol
is provided, its symbol form will be evaluated to a value.
Similarly to the let form, we are dealing with local bindings used to identify
the initialized resources. The macro emits close() calls in nested finally blocks,
so resources initialized successfully are closed even when the body or a later close
operation throws.
Dynamic Scope
A dynamic binding in Clojure temporarily supplies a value for an existing dynamic Var. The symbol continues to resolve lexically to the same namespace-mapped Var, but dereferencing that Var selects the innermost active thread binding. This selection is visible to functions called from the body even when those functions were defined elsewhere—hence the term dynamic scope.
The binding macro pushes the requested thread bindings before evaluating
its body and restores the previous bindings in a finally path. Nested binding
forms therefore override outer ones in stack order. When no thread binding is active,
the Var’s root binding is used.
Thread bindings are not namespace mappings and do not change a Var’s shared root. New threads do not automatically inherit arbitrary thread bindings, although Clojure facilities such as futures and agent dispatch can explicitly convey the caller’s binding frame. Code should rely on the documented behavior of the concurrency facility it uses rather than treating dynamic bindings as process-global state.
Creating Dynamic Bindings
The binding macro, which is discussed in more detail in chapter VII,
is used to create bindings with dynamic scope. Below we will find a usage example
that also demonstrates that bindings of this type are maintained in reference
objects, not in namespaces:
1(def ^:dynamic *x* 5) ; dynamic Var *x*
2
3(def obj-x ; global Var obj-x
4 (var *x*)) ; points to the Var object of *x*
5
6(defn show-off [] ; displays the value of *x*
7 (println " *x* by name:"
8 *x* "\n" ; reading the bound value
9 "*x* by object:"
10 @obj-x "\n")) ; dereferencing the object
11
12(defn test-it [] ; test function
13 (binding [*x* 10] ; dynamic scope of *x*
14 (println "* dynamic scope")
15 (show-off)) ; calling function within the scope
16 (println "* root binding:") ; outside dynamic binding
17 (show-off)) ; calling function outside
18 ; the dynamic scope
19(test-it)
(def ^:dynamic *x* 5) ; dynamic Var *x*
(def obj-x ; global Var obj-x
(var *x*)) ; points to the Var object of *x*
(defn show-off [] ; displays the value of *x*
(println " *x* by name:"
*x* "\n" ; reading the bound value
"*x* by object:"
@obj-x "\n")) ; dereferencing the object
(defn test-it [] ; test function
(binding [*x* 10] ; dynamic scope of *x*
(println "* dynamic scope")
(show-off)) ; calling function within the scope
(println "* root binding:") ; outside dynamic binding
(show-off)) ; calling function outside
; the dynamic scope
(test-it)
See also:
- “Dynamic Vars”, chapter VII.
Destructuring
Destructuring (also called decomposition) is a mechanism for creating bindings of values to symbols, where those values come from structures composed of multiple elements, and specific syntax is used to assign selected values to particular symbols instead of calling functions or macros.
We can use destructuring in the binding vector of the let
special form and its derivatives, in the parameter vector of the
fn form and the defn macro, as well as in constructs that
make use of the aforementioned (e.g., for or doseq). Instead
of symbol binding forms, the first elements of each binding pair will then be
vector binding forms, map binding forms, or even combinations thereof.
To demonstrate the benefits of using destructuring, let us look at a simple example in which we first bind elements of a vector to symbols (using functions that operate on the vector), and then use destructuring for the same purpose.
1(def data [1 2 3]) ; vector with three values
2
3;; manual binding of selected elements to symbols
4
5(let [a (first data)
6 b (first (rest data))
7 c (first (rest (rest data)))]
8 (list a b c))
9; => (1 2 3)
10
11;; destructuring
12
13(let [[a b c] data] (list a b c))
14; => (1 2 3)
(def data [1 2 3]) ; vector with three values
;; manual binding of selected elements to symbols
(let [a (first data)
b (first (rest data))
c (first (rest (rest data)))]
(list a b c))
; => (1 2 3)
;; destructuring
(let [[a b c] data] (list a b c))
; => (1 2 3)
In the second-to-last line we can see that instead of a single symbol, we used
a vector S-expression containing a list of symbols that were bound to the
positionally corresponding values of the vector named data.
Destructuring is a kind of binding of symbols to values. We are still dealing with binding pairs, however:
-
in the place of a single symbol there appears a binding expression containing various symbols (vector binding expression or map binding expression), i.e., a vector or map binding form;
-
the value of the assigned initializing expression is a multi-element structure (e.g., a vector, map, list, record, etc.).
let invocation example
1(let [ ; binding vector:
2 ; - first pair:
3 data ; - symbol (symbol binding form)
4 [1 2 3] ; - initializing expression (vector form)
5 ; - second pair:
6 [a b c] ; - vector binding expression (vector binding form)
7 data] ; - initializing expression (symbol form)
8 (list a b c))
(let [ ; binding vector:
; - first pair:
data ; - symbol (symbol binding form)
[1 2 3] ; - initializing expression (vector form)
; - second pair:
[a b c] ; - vector binding expression (vector binding form)
data] ; - initializing expression (symbol form)
(list a b c))
Positional Destructuring
Positional destructuring (also called positional decomposition) enables creating bindings of symbols to values of selected elements of structures with a sequential access interface (e.g., vectors, lists, or even character strings). It resembles the use of pattern matching and consists of associating symbols given in a certain order with the positionally corresponding elements of the structure provided in the initializing expression.
Positional destructuring can be used in binding vectors of forms such as
let, as well as in
parameter vectors of function definitions.
In fact, we can destructure not only sequences but any collections on which the
nth function can operate.
Vector Binding Form
Using positional destructuring requires placing a vector binding expression in the position where we normally provide a single symbol (as the first element of a binding pair). This should be a vector S-expression containing binding forms (e.g., unquoted symbols) whose positions correspond to the positions of elements in the source structure (given as the initializing expression).
Usage:
[[symbol...] initializing-expression].
let construct
1(let [[a b c] [1 2 3]] ; a -> 1, b -> 2, c -> 3
2 (list a b c))
3; => (1 2 3)
4
5(let
6 [vector [4 5 6] ; binding to a vector
7 sequence (seq vector) ; binding to a sequence based on the vector
8 [a b c] [1 2 3] ; bindings from destructuring a vector expression
9 [d e f] vector ; bindings from destructuring the vector
10 [g ] sequence] ; bindings from destructuring the sequence
11
12 (list a b c d e f g)) ; creating a list with the binding values
13; => (1 2 3 4 5 6 4)
(let [[a b c] [1 2 3]] ; a -> 1, b -> 2, c -> 3
(list a b c))
; => (1 2 3)
(let
[vector [4 5 6] ; binding to a vector
sequence (seq vector) ; binding to a sequence based on the vector
[a b c] [1 2 3] ; bindings from destructuring a vector expression
[d e f] vector ; bindings from destructuring the vector
[g ] sequence] ; bindings from destructuring the sequence
(list a b c d e f g)) ; creating a list with the binding values
; => (1 2 3 4 5 6 4)
In the case of parameter vectors, which we encounter e.g. in function definitions, the initializing expression will be the set of passed arguments.
(defn a-function [a b c]
(list a b c))
(a-function 1 2 3)
; => (1 2 3)
Ignoring Elements
Note that in line 9 of the previous example we provide only one symbol (g),
while the source sequence contains three values (4, 5, 6). As expected, the
first of them will be bound to the symbol. But is there a way to retrieve only
a selected one while ignoring the rest? The _ symbol comes to the rescue here,
indicating that the element at its corresponding position should be ignored.
Usage:
[[... _...] initializing-expression].
_ symbol in positional destructuring
(let [[_ b _] [1 2 3]] ; bindings from destructuring
b) ; evaluating the symbol form
; => 2
It is worth knowing that the _ symbol has special meaning only by convention.
Any other symbol that will not be used can be placed in its stead, and its value
can be overwritten multiple times without affecting the application logic.
Grouping Elements
An interesting case is grouping all remaining, positionally unassigned values into a single, variadic binding. The ampersand symbol placed before the symbol name serves this purpose.
Usage:
[[... & symbol] initializing-expression].
& symbol in positional destructuring
1(let [[_ & remaining] [1 2 3]] ; bindings from destructuring
2 remaining) ; evaluating the symbol form
3; => (2 3)
(let [[_ & remaining] [1 2 3]] ; bindings from destructuring
remaining) ; evaluating the symbol form
; => (2 3)
Accessing the Original Sequence
It may happen that despite destructuring we still need access to the originally
passed data structure of the initializing expression. The :as directive comes
to the rescue here, which should be placed inside the destructuring expression.
Immediately after it there should be an unquoted symbol to which the structure
should be bound.
Usage:
[[... :as symbol] initializing-expression].
:as directive in positional destructuring
1(let [[a b c :as everything] [1 2 3]] ; bindings from destructuring
2 everything) ; evaluating the symbol form
3; => [1 2 3]
(let [[a b c :as everything] [1 2 3]] ; bindings from destructuring
everything) ; evaluating the symbol form
; => [1 2 3]
The :as directive and the symbol assigned to it should be given as the last
pair in the destructuring vector.
Associative Destructuring
Associative destructuring (also called associative decomposition) enables creating bindings of symbols to values originating from selected elements of structures with an associative access interface (e.g., maps or records).
Associative destructuring can be used both in binding vectors (e.g., of the
let special form), as well as in
parameter vectors of function definitions.
Map Binding Form
Associative structures (e.g., maps) express a key-value relationship, and their destructuring consists of specifying keys under which one can find values to be bound to the given symbols.
A map binding expression, which can be called a binding map for short, serves to express this operation. It is a map S-expression that should be placed as the first element of each binding pair in the binding vector, or in place of a single parameter in a function’s parameter vector. The keys of the map can be binding forms (e.g., of symbols, maps, or vectors), and the values are keys of the source structure under which we will find the actual initializing values or further structures.
The source structure from which values will be retrieved for binding to symbols or further destructuring will be the map initializing expression given as the second element of each binding pair.
Usage:
[{symbol key ...} initializing-expression].
1;; B I N D I N G V E C T O R (consisting of pairs)
2;; binding map initializing expression (binding pairs)
3
4(let [ {a :a b :b c :c} {:a 1, :b 2, :c 3} ]
5 (list a b c))
6; => (1 2 3)
;; B I N D I N G V E C T O R (consisting of pairs)
;; binding map initializing expression (binding pairs)
(let [ {a :a b :b c :c} {:a 1, :b 2, :c 3} ]
(list a b c))
; => (1 2 3)
Key specifiers can also be other values, not only keywords.
(let [{a 'a b 'b c 'c} '{a 1, b 2, c 3}]
(list a b c))
; => (1 2 3)
Note that in the example above we quoted the map S-expression so that we would not have to separately quote each symbol within it.
In the case of function parameter vectors, the initializing expression comes from the arguments passed to the function, and the keys are their symbolic names.
1;; PARAMETER VECTOR
2;; binding map
3
4(defn a-function [ & {a :a, b :b, c :c} ]
5 (list a b c))
6
7;; arguments (initializing expression)
8
9(a-function :a 1, :b 2, :c 3)
10; => (1 2 3)
;; PARAMETER VECTOR
;; binding map
(defn a-function [ & {a :a, b :b, c :c} ]
(list a b c))
;; arguments (initializing expression)
(a-function :a 1, :b 2, :c 3)
; => (1 2 3)
Binding Map Keys
If the names of the symbols to which values from the given associative structure
will be bound are to be the same as the key names in that map, we can use
the :keys directive. It allows avoiding repetition and makes the code more
readable.
In the binding map, one should provide a pair whose key is the keyword :keys,
and whose assigned value is a vector S-expression containing unquoted symbols or
keywords designating the keys of the decomposed structure whose values we want
to bind to symbols with the same names.
Usage:
[{:keys [key...]} initializing-expression].
:keys directive
1(let [{:keys [:a :b :c]} {:a 1, :b 2, :c 3}]
2 (list a b c))
3; => (1 2 3)
4
5(let [{:keys [a b c]} {:a 1, :b 2, :c 3}]
6 (list a b c))
7; => (1 2 3)
(let [{:keys [:a :b :c]} {:a 1, :b 2, :c 3}]
(list a b c))
; => (1 2 3)
(let [{:keys [a b c]} {:a 1, :b 2, :c 3}]
(list a b c))
; => (1 2 3)
The keys of the decomposed initializing structure can also be strings or
symbols. In such cases, one can use the :strs or :syms directive instead
of :keys. In both cases, unquoted symbols should be used to specify the names.
Usage:
[{:strs [key...]} initializing-expression],[{:syms [key...]} initializing-expression].
:strs and :syms directives
1(let [{:strs [a b c]} {"a" 1, "b" 2, "c" 3}]
2 (list a b c))
3; => (1 2 3)
4
5(let [{:syms [a b c]} '{a 1, b 2, c 3}]
6 (list a b c))
7; => (1 2 3)
(let [{:strs [a b c]} {"a" 1, "b" 2, "c" 3}]
(list a b c))
; => (1 2 3)
(let [{:syms [a b c]} '{a 1, b 2, c 3}]
(list a b c))
; => (1 2 3)
Accessing the Original Association
It may happen that despite destructuring we still need access to the originally
passed data structure. Similarly to positional destructuring, the :as directive
comes to the rescue. It should be placed in the binding map, and the value
assigned to it must be an unquoted symbol to which the structure of the
initializing expression will be bound.
Usage:
[{:as symbol} initializing-expression].
:as directive in associative destructuring
1(let [{:keys [a b c]
2 :as everything} [1 2 3]] ; bindings from destructuring
3 everything) ; evaluating the symbol form
4; => [1 2 3]
(let [{:keys [a b c]
:as everything} [1 2 3]] ; bindings from destructuring
everything) ; evaluating the symbol form
; => [1 2 3]
The :as directive and the symbol assigned to it should be given as the last
pair in the destructuring vector.
Default Values
In the binding map we can specify default values that will be bound to symbols
if the given keys were not found in the source structure. The :or directive
serves this purpose.
After the keyword :or, a map specifying the default values for keys should be
provided.
Usage:
[{:or {key value ...}} initializing-expression].
:or directive
(let [{:keys [:a :b :c]
:or {a 1, c 3}} {:b 2}]
(list a b c))
; => (1 2 3)
Associative Destructuring of Vectors
It is possible to apply associative destructuring to vectors. In the binding map, instead of keys, one should then provide the positions of elements in the source sequential structure expressed as integers.
Usage:
[{symbol position ...} initializing-expression].
(let [{a 0 b 1 c 2} ["first" "second" "third"]]
(list a b c))
; => ("first" "second" "third")
Vectors as Keys in Destructuring
An interesting example of associative destructuring is a binding map in which the keys are vectors.
(let [{[a b c] :letters} {:letters [1 2 3]}]
(list a b c))
; => (1 2 3)
We can see that the value of the vector key in the map binding form is a keyword
(:letters), which will then be looked up in the map initializing expression,
and positional destructuring will be performed on the found value. This kind
of destructuring is a simple example of the ability to use nested structures in
destructuring forms.
Nested Structures
Destructuring of nested structures is possible thanks to syntax that allows nesting map and vector binding expressions.
1(def personal-data
2 {:first-name "Paul"
3 :last-name "Wilk"
4 :sex :m
5 :contacts {:phones [123456, 543210]
6 :emails ["pw-at-gnu.org"]}})
7
8(let [{:keys [first-name last-name sex], {[phone] :phones
9 [email] :emails} :contacts}
10 personal-data
11 sex-name (if (= sex :m) "male" "female")]
12 (println "Full name: " first-name last-name)
13 (println "Sex: " sex-name)
14 (println "Phone: " phone)
15 (println "Email: " email))
(def personal-data
{:first-name "Paul"
:last-name "Wilk"
:sex :m
:contacts {:phones [123456, 543210]
:emails ["pw-at-gnu.org"]}})
(let [{:keys [first-name last-name sex], {[phone] :phones
[email] :emails} :contacts}
personal-data
sex-name (if (= sex :m) "male" "female")]
(println "Full name: " first-name last-name)
(println "Sex: " sex-name)
(println "Phone: " phone)
(println "Email: " email))
The result of running the above example will be the display of the following lines of text:
Full name: Paul Wilk
Sex: male
Phone: 123456
Email: pw-at-gnu.org
Let us examine the individual fragments of the binding vector to better understand the operations involved. It contains two binding pairs:
- A binding map in which destructuring takes place and its assigned
initializing expression, which is a symbol form (
personal-data) representing a nested map with personal data:
{:keys [first-name last-name sex], {[phone] :phones
[email] :emails} :contacts}
personal-data
- A binding form of a symbol (
sex-name) and its assigned initializing expression, which is theifspecial form (depending on the value bound to thesexsymbol, it emits the appropriate text string):
1sex-name (if (= sex :m) "male" "female")
sex-name (if (= sex :m) "male" "female")
The second pair is not relevant to destructuring, so we will not discuss it further. Let us instead take a closer look at the first pair, where we have a binding map composed of two elements (two key-value pairs):
- The
:keysdirective binding values of the keys:first-name,:last-name, and:sexto the corresponding symbols (from the map identified by the symbolpersonal-data):
1:keys [first-name last-name sex]
:keys [first-name last-name sex]
- A binding map that destructures the structure identified by the key
:contactsfrom thepersonal-datamap:
{[phone] :phones
[email] :emails}
:contacts
We can see that the binding map does not contain simple binding forms (expressed
as unquoted symbols), but rather two vector binding expressions, which
represent another level of destructuring. We are dealing with positional
destructuring, specifically with binding the symbol phone to the first
element of the structure identified by the key :phones and the symbol
email to the first element of the structure identified by the key
:emails. Both of these structures (a vector containing phone numbers and
a vector containing email addresses) should be elements of the map identified
by the key :contacts in the parent structure.
Fully Qualified Keys
Since Clojure version 1.6, we can use keys and symbols with specified namespaces.
1(def personal-data
2 {:first-name "Paul"
3 :contacts/phones [123456, 543210]
4 :contacts/emails ["pw-at-gnu.org"]})
5
6(let [{:keys [first-name],
7 [phone] :contacts/phones,
8 [email] :contacts/emails} personal-data]
9 (println "Name: " first-name)
10 (println "Phone:" phone)
11 (println "Email: " email))
12
13(let [{:keys [first-name contacts/phones contacts/emails]} personal-data]
14 (println "Name: " first-name)
15 (println "Phones:" phones)
16 (println "Emails: " emails))
(def personal-data
{:first-name "Paul"
:contacts/phones [123456, 543210]
:contacts/emails ["pw-at-gnu.org"]})
(let [{:keys [first-name],
[phone] :contacts/phones,
[email] :contacts/emails} personal-data]
(println "Name: " first-name)
(println "Phone:" phone)
(println "Email: " email))
(let [{:keys [first-name contacts/phones contacts/emails]} personal-data]
(println "Name: " first-name)
(println "Phones:" phones)
(println "Emails: " emails))
1(ns user)
2(def personal-data
3 {:first-name "Paul"
4 ::phones [123456, 543210]
5 ::emails ["pw-at-gnu.org"]})
6
7(let [{:keys [first-name ::phones user/emails]} personal-data]
8 (println "Name: " first-name)
9 (println "Phones:" phones)
10 (println "Emails: " emails))
(ns user)
(def personal-data
{:first-name "Paul"
::phones [123456, 543210]
::emails ["pw-at-gnu.org"]})
(let [{:keys [first-name ::phones user/emails]} personal-data]
(println "Name: " first-name)
(println "Phones:" phones)
(println "Emails: " emails))
1(def personal-data
2 {'first-name "Paul"
3 'contacts/phones [123456, 543210]
4 'contacts/emails ["pw-at-gnu.org"]})
5
6(let [{:syms [first-name],
7 [phone] 'contacts/phones,
8 [email] 'contacts/emails} personal-data]
9 (println "Name: " first-name)
10 (println "Phone:" phone)
11 (println "Email: " email))
(def personal-data
{'first-name "Paul"
'contacts/phones [123456, 543210]
'contacts/emails ["pw-at-gnu.org"]})
(let [{:syms [first-name],
[phone] 'contacts/phones,
[email] 'contacts/emails} personal-data]
(println "Name: " first-name)
(println "Phone:" phone)
(println "Email: " email))
In the above example, symbols with specified namespaces were quoted in the binding map, because otherwise they would be treated as symbol forms.
Diagnosing Destructuring
Destructuring complex data collections can carry a risk of error. In such cases it is worth using methods that enable inspecting the destructuring process.
Destructuring to Text, destructure
Thanks to the destructure function we can observe what effect the
destructuring of given structures will have.
Usage:
(destructure bindings).
The function takes one required argument, which should be a binding vector in literal form.
The returned value is a binding vector containing representations of S-expressions used in the destructuring process (literal forms).
destructure function
1(def personal-data
2 {'first-name "Paul"
3 'contacts/phones [123456, 543210]
4 'contacts/emails ["pw-at-gnu.org"]})
5
6(destructure '[{:syms
7 [first-name],
8 [phone] 'contacts/phones,
9 [email] 'contacts/emails} personal-data])
10
11; => [map__10728
12; => personal-data
13; => map__10728
14; => (if
15; => (clojure.core/seq? map__10728)
16; => (clojure.lang.PersistentHashMap/create
17; => (clojure.core/seq map__10728))
18; => map__10728)
19; => vec__10729
20; => (clojure.core/get map__10728 (quote contacts/phones))
21; => phone
22; => (clojure.core/nth vec__10729 0 nil)
23; => vec__10730
24; => (clojure.core/get map__10728 (quote contacts/emails))
25; => email
26; => (clojure.core/nth vec__10730 0 nil)
27; => first-name
28; => (clojure.core/get map__10728 (quote first-name))]
(def personal-data
{'first-name "Paul"
'contacts/phones [123456, 543210]
'contacts/emails ["pw-at-gnu.org"]})
(destructure '[{:syms
[first-name],
[phone] 'contacts/phones,
[email] 'contacts/emails} personal-data])
; => [map__10728
; => personal-data
; => map__10728
; => (if
; => (clojure.core/seq? map__10728)
; => (clojure.lang.PersistentHashMap/create
; => (clojure.core/seq map__10728))
; => map__10728)
; => vec__10729
; => (clojure.core/get map__10728 (quote contacts/phones))
; => phone
; => (clojure.core/nth vec__10729 0 nil)
; => vec__10730
; => (clojure.core/get map__10728 (quote contacts/emails))
; => email
; => (clojure.core/nth vec__10730 0 nil)
; => first-name
; => (clojure.core/get map__10728 (quote first-name))]
We can make the result more readable and present it as code:
1(let [data-map personal-data
2 data-map (if (seq? data-map)
3 (apply hash-map (seq data-map))
4 data-map)
5 phones-vector (get data-map 'contacts/phones)
6 emails-vector (get data-map 'contacts/emails)
7 first-name (get data-map 'first-name)
8 phone (nth phones-vector 0 nil)
9 email (nth emails-vector 0 nil)]
10 {:first-name first-name
11 :phone phone
12 :email email})
13
14; => {:email "pw-at-gnu.org" :first-name "Paul" :phone 123456}
(let [data-map personal-data
data-map (if (seq? data-map)
(apply hash-map (seq data-map))
data-map)
phones-vector (get data-map 'contacts/phones)
emails-vector (get data-map 'contacts/emails)
first-name (get data-map 'first-name)
phone (nth phones-vector 0 nil)
email (nth emails-vector 0 nil)]
{:first-name first-name
:phone phone
:email email})
; => {:email "pw-at-gnu.org" :first-name "Paul" :phone 123456}