Symbols, shadowing, and scope

Clojure resolves symbols to values in a few different ways. The most local is let, which creates temporary bindings that live and die within a single expression. Symbols bound with let can shadow earlier definitions, including core operators:

user=> (let [+ -] (+ 2 3))
-1

The effect stops at the closing parenthesis of the let—outside it, the original meaning of + is back in force:

user=> (+ 2 3)
5

Bindings are sequential, so later entries can reference earlier ones:

user=> (let [cats 3
             legs (* 4 cats)]
         (str legs " legs all together"))
"12 legs all together"

When Clojure sees a let, it substitutes the bound values for the symbols in the body before evaluating. That deferral is the seed of everything else in the language.

Functions defer computation

An expression with no syntactically supplied values for its symbols still compiles fine—it just can’t run on its own. Leaving x unbound turns the expression into a recipe waiting for an input:

(let [x] (+ x 1))

That shape is a function. In Clojure, you write one explicitly with fn:

user=> (fn [x] (+ x 1))
#<user$eval293$fn__294 user$eval293$fn__294@663fc37>

The vector names the function’s arguments; the body is the deferred expression. Once you pass a value, the body evaluates with x bound to that value:

user=> (inc 2)
3
user=> ((fn [x] (+ x 1)) 2)
3

There’s also a compact reader shorthand: #(+ % 1) means the same thing as (fn [x] (+ x 1)), with % standing in for the first argument. More arguments get %1, %2, and so on.

Because functions exist to defer evaluation, defining and immediately invoking one is pointless. The real gain comes from naming a function so it can be reused throughout a program:

user=> (let [twice (fn [x] (* 2 x))]
         (+ (twice 1)
            (twice 3)))
8

Without that name, the computation (* 2 something) appears twice in the fully inlined version of the same program:

user=> (+ (* 2 1)
          (* 2 3))

Functions compress repeated patterns of computation into a single named abstraction. Symbols then let you refer to that abstraction from many places, which is the fundamental organizational act in software engineering.

Vars: mutable bindings

Local bindings are fixed for the lifetime of a let. To redefine a symbol for the whole program—even code you didn’t write—you need a var, created with def:

user=> (def cats 5)
#'user/cats
user=> (type #'user/cats)
clojure.lang.Var

A var is an intermediate reference object. The symbol cats points to the var #'cats, which in turn points to the value "42". That extra layer is the key difference: symbols are immutable references, but vars can be repointed at any time:

user=> (def astronauts [])
#'user/astronauts
user=> (count astronauts)
0
user=> (def astronauts ["Sally Ride" "Guy Bluford"])
#'user/astronauts
user=> (count astronauts)
2

As the example shows, the same symbol can have different meanings at different moments. Mutability on that scale is powerful for REPL experimentation and live system updates, but it’s also hazardous—rebinding a var changes behavior everywhere without a compile error to warn you. Pragmatic Clojure style reserves def for initial setup, with redefinition only under deliberate control.

Named functions

Combining function creation with a var binding is so common it has its own form, defn:

user=> (defn half [number] (/ number 2))
#'user/half

An argument vector is mandatory, even for zero-argument functions. Attempting to call a function with the wrong number of arguments triggers an arity exception:

user=> (half 10)

ArityException Wrong number of args (1) passed to: user$half  clojure.lang.AFn.throwArity (AFn.java:437)

Functions can define multiple arities by providing a series of argument-vector-plus-body clauses instead of a single parameter list:

user=> (defn half
         ([]  1/2)
         ([x] (/ x 2)))
user=> (half)
1/2
user=> (half 10)
5

To accept an arbitrary number of arguments, & collects the remainder into a list. The required parameters come first:

user=> (defn vargs
         [x y & more-args]
         {:x    x
          :y    y
          :more more-args})
#'user/vargs
user=> (vargs 1)

ArityException Wrong number of args (1) passed to: user$vargs  clojure.lang.AFn.throwArity (AFn.java:437)
user=> (vargs 1 2)
{:x 1, :y 2, :more nil}
user=> (vargs 1 2 3 4 5)
{:x 1, :y 2, :more (3 4 5)}

Docstrings attach human-readable explanation to a function. You can include them right after the name in a defn and pull them back out later with doc:

user=> (doc launch)
-------------------------
user/launch
([craft target-orbit])
   Launches a spacecraft into the given orbit by initiating a
   controlled on-axis burn. Does not automatically stage, but
   does vector thrust, if the craft supports it.
nil

The doc command reads the var’s metadata, which defn stores as a map. Inspecting that map directly shows argument lists, docstrings, and even the file and line where the function was defined:

(meta #'launch)
{:arglists ([craft target-orbit]), :ns #<Namespace user>, :name launch, :column 1, :doc "Launches a spacecraft into the given orbit.", :line 1, :file "NO_SOURCE_PATH"}

That metadata trail is directly relevant to the earlier question about type. The built-in functions you call every day are not magic—they are vars with functions attached, carrying the same descriptive baggage and inspectable through the same machinery.

Introspecting functions

We already know that type returns the type of an object:

user=> (type 2)
java.lang.long

And that type, being a function, is itself an object with its own type:

user=> type
#<core$type clojure.core$type@39bda9b9>
user=> (type type)
clojure.core$type

That output shows type is one instance of the type clojure.core$type, living at memory address 39bda9b9. The namespace clojure.core holds the fundamentals of the language, and $type means the name type is defined within it. But the address and type name alone don’t tell us much. To go deeper, we can ask which supertypes describe type:

user=> (supers (type type))
#{clojure.lang.AFunction clojure.lang.IMeta java.util.concurrent.Callable clojure.lang.Fn clojure.lang.AFn java.util.Comparator java.lang.Object clojure.lang.RestFn clojure.lang.IObj java.lang.Runnable java.io.Serializable clojure.lang.IFn}

This set contains every type that type belongs to. The naming matters: type is an instance of clojure.lang.AFunction; it implements or extends interfaces like java.util.concurrent.Callable. Membership in clojure.lang.IMeta tells us it carries metadata, and presence in clojure.lang.AFn confirms its function nature. We can verify the latter explicitly:

user=> (fn? type)
true

Function metadata is also readable, and it often includes documentation:

user=> (doc type)
-------------------------
clojure.core/type
([x])
  Returns the :type metadata of x, or its Class if none
nil

Now the picture sharpens. type accepts one argument, x. If that argument holds :type metadata, the function returns it; otherwise, it falls back to the class of x. A closer inspection of the full metadata gives provenance:

user=> (meta #'type)
{:ns #<Namespace clojure.core>, :name type, :arglists ([x]), :column 1, :added "1.0", :static true, :doc "Returns the :type metadata of x, or its Class if none", :line 3109, :file "clojure/core.clj"}

This functions dates back to Clojure 1.0, and its definition lives in clojure/core.clj at line 3109. Rather than digging up that file by hand, Clojure invites us to read the definition directly:

user=> (source type)
(defn type 
  "Returns the :type metadata of x, or its Class if none"
  {:added "1.0"
   :static true}
  [x]
  (or (get (meta x) :type) (class x)))
nil

There it is: a one-argument function that returns either the value of :type in the metadata, or (class x).

Functions all the way down

The same tools-open any function in Clojure for inspection:

user=> (source +)
(defn +
  "Returns the sum of nums. (+) returns 0. Does not auto-promote
  longs, will throw on overflow. See also: +'"
  {:inline (nary-inline 'add 'unchecked_add)
   :inline-arities >1?
   :added "1.2"}
  ([] 0)
  ([x] (cast Number x))
  ([x y] (. clojure.lang.Numbers (add x y)))
  ([x y & more]
     (reduce1 + (+ x y) more)))
nil

Most functions in a language are built out of simpler functions. The + operator is expressed through cast, add, and reduce1. Often a function definition even references itself-in the case of +, it invokes itself twice, a pattern known as recursion.

At the base of this hierarchy lie primitives that cannot be reduced further. Lisps call these “special forms”. In Clojure, def and the closely related let* (on which let is a thin wrapper) are special forms, defined by the language implementation itself rather than in Clojure code.

user=> (source def)
Source not found

Unlike some dialects of Lisp, Clojure does not aim to build everything atop a tiny set of special forms. Many constructs bottom out in Java functions and types-or, in ClojureScript, in JavaScript. An expression like (. clojure.lang.Numbers (add x y)) ultimately descends into Java. Underneath that sits the JVM, itself written in C and C++, then libraries, the operating system, assembler, microcode, registers, and, at the base, electrons traveling through silicon.

Good language design isolates you from such layers, letting you write entire programs purely in Clojure. But you will occasionally drop to Java for performance or to access tooling from other ecosystems. Exploring Java code is more difficult than Clojure since doc and source do not apply; the available sources and online documentation are the usual guides.

The bigger picture

So far, we’ve seen let bind names to values over a fixed expression, while Vars provide mutable bindings whose definitions can change. Functions generalize expressions: they leave certain values unbound in a shape, and invoking the function binds those variables to arguments, letting evaluation proceed.

Functions break a program into smaller, named pieces that express themselves in terms of one another. Thoughtful naming makes the meaning of functions-and values-clear at a glance.

We also took doc and source for a spin, exposing definitions of fundamental functions inline. The Clojure cheatsheet enumerates the core functions and serves as a starting point when you know the problem but not the right tool for it. Chapter 4, Sequences, will tour a broad selection of them. My thanks go to Zach Tellman, Kelly Sommers, and Michael R Bernstein for their careful reading of this chapter.