✓ 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:
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:
Dereference all the symbols.
Convert all function invocations to their return values.
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.
For \(\sigma\not=\mathrm{toplevel}\), we have
An example¶
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¶
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
Dynamic scoping is not recommended¶
Clojure discourages dynamic scoping. It places several restrictions.
Non-parameter symbols must be top-level symbols.
When invoking the function, a special
bind
-form is needed to inject bindings into the evaluation scope.
;;
;; dynamic scoping
;;
(def ^:dynamic username "kenpu")
(defn greeting [message]
(println (format "[%s]: %s" username message)))
#'user/greeting
;;
;; default username
;;
(greeting "hello")
[kenpu]: hello
nil
;;
;; inject a modified username into the evaluation scope
;;
(binding [username "albert_einstein"]
(greeting "loves physics"))
[albert_einstein]: loves physics
nil