Bindings allow us to identify in-memory objects used in our programs (to give them stable identities), while namespaces make it possible to manage visibility and encapsulate fragments of source code. In this installment, we will learn how to understand these mechanisms in Clojure and how to use them.
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, is a shorthand term for the process of
associating in-memory objects (data or subroutines) with identifiers. For
example, the letter a can be bound to a memory cell storing the value 123.
(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.
Even if there existed a computer in which memory addresses were alphanumeric and defined by the programmer, bindings would still be useful because they would decouple data from their location in memory. A binding can be changed while the program is running, whereas a memory address (even the most readable one) cannot. This topic 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.
Programming languages may differ in how bindings are created depending on the processing stages at which they arise. We will therefore deal with static binding, also known as early binding, which takes place before the program runs (e.g., during its loading or at compile time), as well as dynamic binding, also known as late binding, where name-to-object bindings are created at runtime.
In Clojure we deal with both of the above-mentioned types of bindings, although the more accurate terms in this case are “static binding” and “dynamic binding,” since when distinguishing between them we do not emphasize the stage of source code translation but rather the characteristics of the bindings themselves (e.g., whether they can be updated).
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 that depends on placement is called lexical scope, and a scope that
depends on state is called dynamic scope. In the latter case, a special binding
stack is maintained for each identifier; this stack may change over time depending on
the context and the operations performed on the binding. For example, within a
function we can refer to a dynamic variable named d defined outside the function
whose value was changed just before the function call. If the scope of d were
lexical, we would always refer within the function to the value of d from the
moment the function was defined (in the case of languages supporting
so-called closures), or it would be unbound to any value within the
function.
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,
- unrestricted – expressed by global Vars.
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;; unrestricted scope
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) ; binding a dynamic Var to a value
15a ; using the current value identified by the symbol
16(binding [a 789] ; dynamically changing the current value
17 a) ; using the changed current 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
;; unrestricted scope
(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) ; binding a dynamic Var to a value
a ; using the current value identified by the symbol
(binding [a 789] ; dynamically changing the current value
a) ; using the changed current 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 there is a namespace mechanism. Thanks to it, one can organize individual program elements and separate symbolic identifiers that originate from different sources in order to avoid naming conflicts.
A namespace is, in the abstract sense, a dictionary of terms, each of which should be unique within it. In a more approachable way, we can describe a Clojure namespace as a uniquely named container for storing globally accessible bindings.
Programs written in Clojure that create bindings with unrestricted scope (used to identify functions or references to values) will always use namespaces because that is where bindings are placed.
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 it should set the
special reference identified by the symbol *ns* to a value corresponding to an
object of type clojure.lang.Namespace, that is, to a namespace. If the namespace
with the given name (in this example smangler.api) does not yet exist, it will be
created. The consequence of this operation is that every binding with unrestricted
scope created thereafter will be placed in that namespace, or more precisely, in a
special map contained within its object.
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)
Every unquoted symbol used in the source file after the occurrence of ns that has
not been lexically bound will be looked up in the current namespace (indicated by the
current value of *ns*) in order to find the construct associated with it that holds
a reference to some value.
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 mirrored in the current namespace if
their names are sufficiently unique, and may even rename them.
Qualified Namespace
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 store:
- identifiers of global Vars,
- identifiers referring to Java classes,
- identifiers 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/dodaj), 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 of elements in which the keys are also symbols, and their associated values can be:
- objects of type
Var, - references to Java classes.
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.
References to Var objects from other namespaces are created using the
function refer or use, references 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
Worth noting is the fact that mappings in a namespace can be marked as private. They will then not be visible outside the namespace in which they were placed, meaning:
-
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 functions:
-
in-ns– used to set the current namespace, -
import– assigns the class names of a given Java package to identifiers in 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 in constant form and returns
a namespace object (of type clojure.lang.Namespace).
create-ns function
(create-ns 'nowa)
; => #<Namespace nowa>
See also:
Using Namespaces
Using namespaces involves referring to various objects by means of symbolic forms,
that is, symbols in unquoted form that identify other objects. By placing such a symbol
in the program’s source code, we cause the appropriate namespace to be searched (using
the resolve function), and then the value of the object identified by the symbolic
name is retrieved.
Let us recall that symbols can have a qualified namespace or contain no namespace information. This property allows using 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.
After the search operation, the object associated with the symbolic name will be
retrieved from the namespace. It will be either a global Var of type Var or a Java
class. If we are dealing with a reference to an object in another namespace, it will
be used to obtain it.
Setting the Current Namespace, in-ns
The current namespace can change dynamically at the programmer’s will. The reference to
it resides in a global dynamic variable named *ns*. We are
therefore able, within a given source file (or even within an expression passed to the
binding macro), to switch the namespace by replacing the value of this variable
in the chosen context. This context need not be lexical, although 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 constant form of a symbol. If the
namespace designated by it does not yet exist, the function create-ns will be called
to create it. After the function executes, the binding of the dynamic, global variable
*ns* will be replaced and the namespace object will be returned.
in-ns function
(in-ns 'nowa)
; => #<Namespace nowa>
After executing the above code in the REPL, we may be surprised that the interpreter
does not “see” language functions that were previously reachable by providing their
symbolic identifiers. This happens because we did not import 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.
Determining the Name, namespace
Thanks to the function namespace, we can obtain the namespace name from a given
symbol (in literal form) that contains a namespace name.
Usage:
(namespace symbolic-name).
This function does not perform a namespace search; it simply extracts from the symbol passed as an argument the appropriate component of its name.
The return value is a text string or nil if we are dealing with a symbol without
a qualified namespace.
namespace function
(namespace 'przestrzeń/jakaś-nazwa)
; => "przestrzeń"
Resolving by Names, ns-resolve
Thanks to the function ns-resolve, we can obtain the object identified by
a symbol if a symbol of that name has been assigned to it in the namespace given as
the first argument (as a literal symbol or namespace object). The last argument is
a symbol in constant form that identifies the sought object.
The function returns an object with the given name expressed as a literal symbol, or
the value nil if no mapping was found in the given namespace.
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 'cośtam) ; => nil
5(ns-resolve *ns* #{'replace 'coś} '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 'cośtam) ; => nil
(ns-resolve *ns* #{'replace 'coś} '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 the global variable *ns*). However, one can provide a symbol in
constant form with a qualified namespace, and the function will use that
information. The return value is an object of type Var.
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.core/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.core/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 (an object of type
clojre.lang.IteratorSeq) containing all defined namespaces in the form of the
objects representing them.
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
in constant form, 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 in constant form.
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
(remove-ns 'clojure.string)
; => nil
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 allows adding references to Var objects placed in other
namespaces within the current namespace. As its first argument it takes the name of
the source namespace in the form of a constant symbol, 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 'nazad'
19
20(refer 'clojure.string
21 :exclude '[replace]
22 :rename '{reverse nazad})
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 'nazad'
(refer 'clojure.string
:exclude '[replace]
:rename '{reverse nazad})
; => 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 [] "aplikacja")
;; 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 [] "aplikacja")
Adding References to Java Classes, import
The import macro allows adding to a namespace mappings of symbols to references
referring to Java classes located in a given package.
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 function 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 function
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 function 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 in constant form or a namespace object, and a variable name in the form of
a constant symbol.
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 'zmienna) ; => #'user/zmienna
2(intern 'user 'zmienna 5) ; => #'user/zmienna
3user/zmienna ; => 5
4zmienna ; => 5
5(intern (find-ns 'user) 'a 10) ; => #'user/a
(intern 'user 'zmienna) ; => #'user/zmienna
(intern 'user 'zmienna 5) ; => #'user/zmienna
user/zmienna ; => 5
zmienna ; => 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 zmienna) ; => #'user/zmienna
2(intern 'user '^{:private true} zmienna-2) ; => #'user/zmienna
3(intern 'user (with-meta 'zmienna-3 {:private true})) ; => #'user/zmienna
(intern 'user '^:private zmienna) ; => #'user/zmienna
(intern 'user '^{:private true} zmienna-2) ; => #'user/zmienna
(intern 'user (with-meta 'zmienna-3 {:private true})) ; => #'user/zmienna
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. The symbol in
this context will not create a symbolic form or a constant form, but rather
a binding form.
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 function 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 function
1(def zmienna) ; => #'user/zmienna
2(def zmienna 5) ; => #'user/zmienna
3(def zmienna "dokumentacja" 5) ; => #'user/zmienna
4user/zmienna ; => 5
5zmienna ; => 5
6
7;; accessing the documentation – the doc function
8
9(doc zmienna)
10; >> user/zmienna
11; >> dokumentacja zmiennej
12; => nil
(def zmienna) ; => #'user/zmienna
(def zmienna 5) ; => #'user/zmienna
(def zmienna "dokumentacja" 5) ; => #'user/zmienna
user/zmienna ; => 5
zmienna ; => 5
;; accessing the documentation – the doc function
(doc zmienna)
; >> user/zmienna
; >> dokumentacja zmiennej
; => 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 function to create a private global Var
1(def ^:private zmienna) ; => #'user/zmienna
2(def ^{ :private true } zmienna 5) ; => #'user/zmienna
(def ^:private zmienna) ; => #'user/zmienna
(def ^{ :private true } zmienna 5) ; => #'user/zmienna
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 used for testing (the variable object will
be accessible in it as an fn literal placed in the metadata) |
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 |
PersistentVector$ChunkedSeq |
A vector sequence with arguments, if the object refers to a function or macro |
Global Vars can be updated by, among other means, calling the def function
again. This involves binding the reference inside the Var object to a new
value. If the variable identified by the given symbol already exists, its object in
the namespace will not be replaced by another one, but its reference to a specific
value (the root binding) will be 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 allows creating a Var object and placing it in the
current or a specified (by an unquoted symbol) namespace. It works similarly to
def, but does not update the binding when the Var object already
has one.
The macro does not allow setting documentation strings because it accepts only two arguments: an unquoted symbol (which may contain namespace information) and an expression whose value, once evaluated, will become the value of the variable’s root binding.
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 when naming function objects and everywhere one needs to
refer to a constant value that should be the result of the first evaluation of some
expression or the first retrieval of data from an external source.
Note: Even if the binding to a value did not take place, the metadata originating from the passed symbol will be set and will replace the previous ones.
Usage:
(defonce symbol expression).
defonce function
1(defonce zmienna) ; => #'user/zmienna
2(defonce zmienna 5) ; => #'user/zmienna
3(defonce ^:flaszka zmienna 1000) ; => #'user/zmienna
4user/zmienna ; => 5
5zmienna ; => 5
6
7(meta #'zmienna)
8; => { :ns #<Namespace user>, :name zmienna,
9; => :flaszka true, :file "NO_SOURCE_PATH",
10; => :column 1, :line 1 }
(defonce zmienna) ; => #'user/zmienna
(defonce zmienna 5) ; => #'user/zmienna
(defonce ^:flaszka zmienna 1000) ; => #'user/zmienna
user/zmienna ; => 5
zmienna ; => 5
(meta #'zmienna)
; => { :ns #<Namespace user>, :name zmienna,
; => :flaszka 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 bindings of symbols
to global Vars or Java classes. It takes two arguments. The first specifies
the namespace (using a symbol in constant form 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, we also showed that when a mapping is removed from a namespace,
the Var class object itself (or another object) is not destroyed but loses its name.
Adding Aliases, alias
The alias mechanism allows referring to different namespaces using alternative identifiers placed in the current namespace.
The function alias allows adding alternative names for other namespaces to the
current namespace. It takes two arguments: the first should be a symbol in constant
form, and the second a namespace object or its name expressed as a literal symbol. 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 constant symbol form 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
namespace objects to symbolic identifiers. If the namespace does not exist, an
exception will be thrown.
Usage:
(ns-aliases namespace).
ns-aliases function
1(ns-aliases 'clojure.core) ; => {jio #<Namespace clojure.java.io>}
2(alias 'stri 'clojure.string) ; => nil
3(ns-aliases 'user) ; => {stri #<Namespace clojure.string>}
(ns-aliases 'clojure.core) ; => {jio #<Namespace clojure.java.io>}
(alias 'stri 'clojure.string) ; => nil
(ns-aliases 'user) ; => {stri #<Namespace clojure.string>}
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
1(ns-interns 'user)
2; => {apropos-better #'user/apropos-better, cdoc #'user/cdoc,
3; => find-name #'user/find-name, help #'user/help,
4; => clojuredocs #'user/clojuredocs}
(ns-interns 'user)
; => {apropos-better #'user/apropos-better, cdoc #'user/cdoc,
; => find-name #'user/find-name, help #'user/help,
; => clojuredocs #'user/clojuredocs}
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 imported into the current namespace (e.g., using
fn-refer).
Usage:
(ns-refers namespace).
ns-refers function
1(ns-refers 'user)
2; => {primitives-classnames #'clojure.core/primitives-classnames,
3; => +' #'clojure.core/+', …}
(ns-refers 'user)
; => {primitives-classnames #'clojure.core/primitives-classnames,
; => +' #'clojure.core/+', …}
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
(ns-imports 'user)
; => {Enum java.lang.Enum, InternalError java.lang.InternalError, …}
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 (reference variables
of type Var and Java classes). If the namespace does not exist, an exception will be
generated.
Usage:
(ns-map namespace).
ns-map function
(ns-map 'user)
; => {primitives-classnames #'clojure.core/primitives-classnames, … }
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 public mappings of symbolic identifiers
to objects. If the namespace does not exist, an exception will be generated.
Usage:
(ns-publics namespace).
ns-publics function
1(ns-publics 'user)
2; => {apropos-better #'user/apropos-better, cdoc #'user/cdoc,
3; => find-name #'user/find-name, help #'user/help, clojuredocs #'user/clojuredocs}
(ns-publics 'user)
; => {apropos-better #'user/apropos-better, cdoc #'user/cdoc,
; => find-name #'user/find-name, help #'user/help, clojuredocs #'user/clojuredocs}
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.
In Clojure, we will most commonly deal with source code libraries in Java archives
(JARs) containing exclusively code written in Clojure. In some rare cases, we may
encounter libraries that instead of source code in Clojure will contain files
compiled to bytecode (.class files).
Loading Libraries
Loading software libraries and subsequently placing the needed references in the
current namespace requires making use of several functions and macros presented
earlier (e.g., refer or import). Fortunately, programmers do
not have to toil too much, because there are macros that allow one to express
concisely what should be loaded and what additional operations need to be performed
on namespaces.
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 macro
The require macro loads external software libraries. Each argument provided 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 (symbolic-names)– uses thereferfunction and for a sequence of symbolically expressed names creates references in the current namespace (providing the key:allmeans requesting the creation of references to all public global variables);
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 allow influencing the behavior of the macro. 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 macro works in such a way that for each library being loaded, a namespace and a corresponding Java package are created – their names are constructed based on the given symbolic name. Loading a library is in essence reading its root file, located in the library’s root directory. The root file name 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 macro
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 macro
The use macro works exactly like require and is invoked in the same
way, but references to every global variable from the loaded library are
automatically added to the current namespace, using the refer function.
The use macro 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…).
Since Clojure release 1.4, it is recommended to use the require or
ns macro with appropriate parameters instead of use.
The ns macro
The ns macro was created so that one would not have to invoke other macros and
functions related to namespace handling, but rather group all important operations
in one place (e.g., in the header section of a source code file).
The macro allows one to set the current namespace (create it if it does not yet exist and switch to it), and then load needed source code files, import all or selected mappings, and generate pseudocode for given classes.
The macro takes the name of the namespace that will be set as current, as well as an optional set of so-called reference clauses, which may contain commands for performing additional operations. The clauses should be grouped in a list S-expression containing keywords serving as their names. Arguments provided after keys do not need to be quoted – after evaluation they will be passed to the invoked functions or macros.
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.
If AOT compilation is not taking place, the :gen-class clause is ignored. If this
clause was not used but compilation is occurring, only the code for
namespace-name__init.class will be produced.
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-fun 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
Thanks to bindings, we can identify objects placed in memory in Clojure. This identification will consist 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.
In the case of binding forms we are dealing with bindings of symbolic identifiers to values. Their values cannot be updated, but it is possible to shadow them by binding a symbol of the given name to a different value in some context (e.g., lexical or dynamic).
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
Technically speaking, in Clojure we can encounter three main types of bindings:
- symbol bindings to values,
- reference object bindings to values,
- dynamic Var bindings to values.
Dynamic Vars are handled by one of the reference types (Var) – the same one
that is used to handle global Vars – however we distinguish them separately,
because they are characterized by so-called dynamic scope.
Symbol Bindings
Symbol bindings serve to identify values or reference objects in certain contexts. We can distinguish symbol bindings:
-
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
and additionally:
- in abstract structural bindings:
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 type object bindings serve to track changing, shared states expressed by different values over time. The places where binding information is stored are reference objects, e.g.:
- global Vars, local
and dynamic (type
Var), - Atoms (type
Atom), - Agents (type
Agent), - Refs (type
Ref), - Futures (type
Future), - Promises (type
Promise), - Delays (type
Delay), - Volatiles (type
Volatile).
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 then called dynamic Vars. They
differ from regular global Vars in the way the Var type object is initialized,
and making use of a dynamic binding is realized using the binding form
binding and constructs that make use of it.
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 can encounter several kinds of scopes: indefinite, lexical, and dynamic.
Indefinite Scope
In the case of symbolically identified global Vars or Java classes, we will speak of indefinite scope, meaning the potential ability to refer to the indicated objects from any place in the program.
Thanks to this kind of scope we are able to express global, shared states in programs, which will be identified by constant names. Values may change over time, but the identities identifying them will remain constant throughout the program.
An example of widespread use of indefinite scope 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 nasza) ; switch namespace
2(def x 5) ; global Var x (root binding with value 5)
3(defn funk [] x) ; function that returns the value of global Var x
4(funk) ; function call
5; => 5 ; result of the call
6
7(ns inna) ; switch namespace
8(funk) ; attempt to call the function
9; >> Unable to resolve symbol: funk in this context
10
11(nasza/funk) ; calling the function with a namespace-qualified symbol
12; => 5
(ns nasza) ; switch namespace
(def x 5) ; global Var x (root binding with value 5)
(defn funk [] x) ; function that returns the value of global Var x
(funk) ; function call
; => 5 ; result of the call
(ns inna) ; switch namespace
(funk) ; attempt to call the function
; >> Unable to resolve symbol: funk in this context
(nasza/funk) ; calling the function with a namespace-qualified symbol
; => 5
Let us note that after switching the current namespace to inna we lost
visibility of the value bound to the global Var funk 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 constant 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 allows creating 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 in the case of let we are not dealing with Var type
objects, but with local bindings used to identify assigned values. Bindings
of symbols to values created in the binding vector are stored on a special
local binding stack, while new values arising as a result of evaluating
initialization expressions occupy the heap space of the program.
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 allows creating 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 allows creating 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 allows creating
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 allows creating 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 allows creating 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 funk [a b] ; definition of function funk; 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 funk [a b] ; definition of function funk; 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 bindings created in the binding vector of the
with-local-vars form is limited to the S-expressions given as its arguments.
An attempt to refer in an initialization expression of the vector to a variable
bound to a symbol at an earlier position of the same vector will result in an
exception being thrown.
See also:
- “Local Vars”, chapter VII.
I/O Binding, with-open
The with-open macro allows creating lexical scope for bindings in a similar way
to let. Lexical bindings will also be created, whose scope will be
limited to the S-expressions given as passed arguments, but all initialization
values of the bindings after evaluation must be Java objects for which the
close() method can be called.
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
constant values.
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 assigned values. All these values must be Java objects for which the
close() method can be called. The macro will call it in a finally block, and
thus regardless of whether the evaluation of expressions succeeds or not. Thanks
to this, any open file handles, sockets, or perhaps even database connection
objects will be closed after the evaluation of the expressions passed to the
macro.
Dynamic Scope
Dynamic scope in Clojure is a scope in which we are dealing with shadowing of values of existing global Var bindings (of indefinite scope) by maintaining a global binding stack for each of them. This happens independently of the lexical context and requires the use of a special form.
The binding stack is a structure whose task is to handle shadowing of the current global Var’s value in a certain execution context limited by time.
If there is a global Var for which at some point a dynamic scope is created by
the programmer, then on the stack assigned to this Var a new binding with
a value is placed. It will be removed from the stack only when the evaluation of
the expression in which the dynamic binding was established is completed (in the
case of Clojure, the body of the binding macro, which will be
discussed further).
If during program execution in which we have a global Var with dynamic scope another shadowing of it appears (caused by the introduction of a new dynamic scope), the binding again goes onto the stack associated with the given Var.
Every reference to a global Var for which a non-empty stack of dynamic bindings exists results in returning the value that the last (most recent) binding on that stack refers to. This happens independently of the lexical context. We can therefore call a dynamic binding one that lasts for a certain time, as opposed to lexical bindings, which cover certain areas of source code.
When there is no dynamic binding on the stack, the so-called root binding of the global Var is used.
Dynamic bindings of global Vars are realized by shadowing values in the bindings
of Var type reference objects representing those Vars, and not by shadowing
symbol mappings in namespaces. Furthermore, dynamic shadowings of global Vars are
always performed in the current thread of execution. If in other threads no
dynamic binding was created (using the binding construct), the Var will retain
its current root binding with a value in them.
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 "* indefinite scope:") ; dynamic
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 "* indefinite scope:") ; dynamic
(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 doall). 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 both in binding vectors of special forms
(such as let or binding), 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 or the binding macro), 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}