✓ 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.

Scopes

Symbols

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

Binding

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 \]

where

  • \(s\) is a symbol

  • \(v\) is data

Symbol dereference

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

Scope

  • 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.

image.png

image.png

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.

Definition

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

image.png

Scopes in Clojure

Top-level bindings

(def pi 3.1415)
#'user/pi
(println pi)
3.1415
nil
(def greeting (fn [message] (println "Hello," message)))
#'user/greeting
(greeting "how are you?")
Hello, how are you?
nil
(defn greeting [message] (println "Hello," message))
#'user/greeting
(greeting "how are you?")
Hello, how are you?
nil

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
nil

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

instructor
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))))
#'user/area-of-circle
(area-of-circle 10.)
Area of a circle with radius 10.00 is 314.15
nil

Deferred evaluation

image.png

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))))
#'user/add
(add 10)
15