✓ Symbols, Bindings and Scopes in Clojure

In this section, we will present two important concepts in functional programming:

  • Functions, and

  • Scopes.

We will use Clojure to illustate the interplay between the two concepts, and how they should be used in functional programming.



A symbol is a name that is a reference to some data.


An association between a symbol and data is called a symbol binding or simply binding.

We will write a symbol binding as:

\[ s \mapsto v \]


  • \(s\) is a symbol

  • \(v\) is data

Symbol dereference

Dereference of a symbol is to retrieve the data that the symbol is bound to.


  • A scope is a collection of symbol bindings.

  • For a scope \(\sigma\), its domain is the set of symbols defined in the scope.

  • Given a symbol \(x\in\mathrm{dom}(\sigma)\), the dereference of \(x\) in \(\sigma\) is written as \(\sigma(x)\).

Structures of Scopes

Evaluating expressions

Every expression is inside some scope.

Evaluation of an expression involves:

  1. Dereference all the symbols.

  2. Convert all function invocations to their return values.

Nested Scopes

Scopes are nested, and form a tree.



Closure of a scope

Each scope \(\sigma\) has a unique parent scope \(\mathrm{parent}(scope)\). We arbitrarily define \(\mathrm{parent}(\sigma) = \mathrm{nil}\) if \(\sigma\) is the top-level scope.


The closure of a scope is a set of symbol bindings defined by all the ancestors of a scope.

\[ \mathrm{closure}(\mathrm{toplevel}) = \mathrm{toplevel} \]

For \(\sigma\not=\mathrm{toplevel}\), we have

\[\begin{split} \mathrm{closure}(\sigma)(x) = \left\{ \begin{array}{ll} \sigma(x) & \mathrm{if}\ x\in\mathrm{dom}(\sigma) \\ \mathrm{closure}(\mathrm{parent}(\sigma))(x) & \mathrm{otherwise} \end{array} \right. \end{split}\]

An example


Scopes in Clojure

Top-level bindings

(def pi 3.1415)
(println pi)
(def greeting (fn [message] (println "Hello," message)))
(greeting "how are you?")
Hello, how are you?
(defn greeting [message] (println "Hello," message))
(greeting "how are you?")
Hello, how are you?

Sub-scope using let-form

Clojure allows us to create sub-scopes with additional local bindings using a let-form:

(let [ bindings... ] expression)
(let [instructor "Ken Pu"
      title "Programming languages"
      code "CSCI 3055U"]
    ;; in the body we have access to 
    ;; local bindings **and**
    ;; top-level bindings
    (greeting (str instructor " teaches " code ": " title))
    (println "PI =" pi))
Hello, Ken Pu teaches CSCI 3055U: Programming languages
PI = 3.1415

But note that the bindings for instructor, title and code are all just local to the scope inside the let-form.

Syntax error compiling at (REPL:0:0).
Unable to resolve symbol: instructor in this context
  Util.java:   221 clojure.lang.Util/runtimeException
   core.clj:  3214 clojure.core$eval/invokeStatic
   core.clj:  3210 clojure.core$eval/invoke
   main.clj:   437 clojure.main$repl$read_eval_print__9086$fn__9089/invoke
   main.clj:   458 clojure.main$repl$fn__9095/invoke
   main.clj:   368 clojure.main$repl/doInvoke
RestFn.java:  1523 clojure.lang.RestFn/invoke
   AFn.java:    22 clojure.lang.AFn/run
   AFn.java:    22 clojure.lang.AFn/run
Thread.java:   832 java.lang.Thread/run

A remark on let-forms

Something quite obvious but subtle about the let-form is that the definition and the evaluation of the let-form are the same.

This is called immediate evaluation. We will later explore other evaluation modes including: deferred, lazy and asynchronous evaluations.

Sub-scopes in function body

Another way of creating a sub-scope is by function declaration.

The body of the function is a new scope, with additional bindings from the parameters of the function.

(fn [parameters...] <body>)
(defn area-of-circle [radius]
    ;; inside the body, we have access to `radius` symbol binding
    (let [area (* pi radius radius)]
        ;; inside the let-form, we have access to both radius and area bindings
        (println (format "Area of a circle with radius %.2f is %.2f" radius area))))
(area-of-circle 10.)
Area of a circle with radius 10.00 is 314.15

Deferred evaluation


Function bodies are evaluated in a different scope from its definition.

This is called deferred evaluation.

Scope of function body

How should we determine the bindings available in the evaluation scope of the function body?

  • Parameters override bindings in the closure.

Lexical Scoping Rule

Non-parameter symbols are taken from the closure of the declaration scope.

This is the default behaviour.

Dynamic Scoping Rule

Non-parameter symbols are taken from the closure of the evaluation scope.

Putting it all together

Lexical and Dynamic Scoping in Clojure

Lexical scoping is the default

;; Lexical scoping - easy and natural

(def add
    (let [x 1
          y 2
          z 3]
        (fn [x] (+ x y z))))
(add 10)