Programming languages provide us with a set of tools to work with data, perform operations on it, and automate various tasks. Two of the most important concepts in programming are lexical scoping and closures. These concepts are closely related and provide us with powerful ways to write code that is concise, readable, and reusable. Understanding lexical scoping is essential for understanding closures, and both concepts are fundamental to writing efficient and effective code in many programming languages.
Table of Contents
Open Table of Contents
What is Lexical Scoping?
The body of a function is evaluated in the environment where the function is defined, not the environment where the function is defined called. This is the lexical scoping.
Let’s consider an example:
x = 9
def foo_env(y):
x = 100
def foo(z):
print(x + z)
foo(y)
def bar(y):
print(x + y)
def bar_call_env(y):
x = 99
bar(y)
foo_env(10)
bar_call_env(10)
In this Python code, we define a global variable x
with a value of 9. The foo_env
function defines a local variable x
with a value of 100, and a nested function foo
that accesses the value of x
and adds it to its parameter z
. The foo_env
function calls the foo
function with an argument of 10, resulting in the value of x
(100 in its lexical environment) being added to 10 and printed, resulting in an output of 110. The bar
function is defined in a global environment and simply adds the value of x
in that environment to its parameter y
and prints the result. When called with an argument of 10, it adds the value of x
in the global environment (9) to 10 and prints the result, resulting in an output of 19. Both functions foo
and bar
are essentially the same. However, they differ in where they were defined. foo
is defined in a local environment inside of foo_env
as a nested function and called, and bar
is defined in a global environment. This demonstrates how functions use the values of variables that are in the same environment where the function is defined, which is a key feature of lexical scoping.
Sidenote: The opposite of lexical scoping is dynamic scoping where the environment when the function call is made is used.
Why Lexical Scope?
There are many good reasons for having lexical scope
-
The meaning of a function does NOT depend on local varaible names used and one can go ahead rename the local variables without affecting anything.
-
Functions can be type-checked and reasoned about where defined. This makes testing much more easier.
-
Closures require lexical scoping, so…
What are Closures?
Now, the question arises how does the language implementation know which free variables to use? The answer is function closure or just closure. While the code itself can have free variables (variables that are not bound inside the code so they need to be bound by some outer environment), the closure carries with it an environment that provides all these bindings. So the closure overall is “closed” — it has everything it needs to produce a function result given a function argument.
The creation of a closure varies greatly depending on the language implementation. For instance, in Standard ML, a closure is formed when a function is evaluated. On the other hand, in the majority of mainstream programming languages that have first-class functions (ie functions can be passed as arguments, returned from other functions, or assigned to variables), a closure can be created by returning a nested inner function from an outer function. This results in the local environment of the outer function enclosing the inner function, forming a closure.
# A simple closure example in python
def outer_func(msg):
msg_3_times = msg * 3
def inner_func():
# msg_3_times is a free variable here
# due to lexical scoping, inner_func() can access msg_3_times
print(msg_3_times)
# following line returns inner_func() as a closure
# inner_func() environment is preserved even after outer_func() has finished executing
return inner_func
# hi_func and bye_func are closures defined
# with different msg values
hi_func = outer_func("Hi")
bye_func = outer_func("Bye")
hi_func() # prints Hi
bye_func() # prints Bye
Why are Closures a big deal?
Closures are a big deal because:
-
they allow you to create functions with persistent state that can be accessed by other functions.
-
they can be used to effectively abstract the implementation details and prevent polluting the global environment, as anything that a function needs can be enclosed in a closure.
This makes it possible to write code that is more modular and reusable. Some common uses include creating decorators, implementing callbacks, and creating factory functions. They are a powerful tool for writing efficient and effective code in many programming languages. A simple example:
# Function that returns a closure that adds a suffix to the word
def make_suffixer(suffix):
def suffixer(word):
return word + suffix
return suffixer
# Create two suffixers
chan_suffixer = make_suffixer("-chan")
kun_suffixer = make_suffixer("-kun")
# Use the suffixers
print(chan_suffixer("Sakura")) # prints Sakura-chan
print(kun_suffixer("Sakura")) # prints Sakura-kun
print(chan_suffixer("Naruto")) # prints Naruto-chan
print(kun_suffixer("Naruto")) # prints Naruto-kun
Now, go ahead and use your new powers of lexical scoping and closures to create amazing code…