Dynamic scoping in Lua
What is dynamic scoping
Dynamic scoping is a programming language paradigm that you don’t typically see. The scoping that most programmers are used to is called lexical scoping. It’s found in Lua and many other languages. Lexical scoping is the dominant choice for a reason: it’s easy to reason about and understand just by looking at the code. We can see what variables are in scope just by looking at the structure of the text in our editor. Scoping controls how a variable’s value is resolved.
Dynamic scoping does not care how the code is written, but instead how it executes. Each time a new function is executed, a new scope is pushed onto the stack. This scope is typically stored with the function’s call stack. When a variable is referenced in the function, the scope in each call stack is checked to see if it provides the value.
Using the syntax
dynamic(var) to represent a dynamic scope variable lookup:
local function make_printer() local a = 100 return function() print("Lexical scoping:", a) print("Dynamic scoping:", dynamic(a)) end end local function run_func(fn) local a = 200 fn() end local print_a = make_printer() run_func(print_a) -- prints: -- Lexical scoping: 100 -- Dynamic scoping: 200
In this example we're priting a variable named
a with each of the scoping
styles. With lexical scoping it’s very easy to see that we've created a closure
on the variable
a. That variable is bound to the scope of
the way the code blocks have been written nest the scopes.
With dynamic scoping things are a bit different. Each entry in the callstack
represents a different scope to check for the variable
a. Because there is no
a defined in the function referencing it, we traverse up the call stack to
find a declared variable. It’s found in the body of
run_func, where the value
The usefulness of this scoping may not be immediately clear. It may seem very error prone because the value of the variable we're requesting can come from any caller’s stack, even code that we haven’t event written.
The power of dynamic scoping is that we can inspect the calling context to control the behavior of our functions.
Implementing dynamic scoping
We can implement dynamic scoping fairly easily in Lua through the
library. We'll mimic the example above with a function called
takes the name of a variable, as a string, to look up dynamically.
In Implementing setfenv in Lua 5.2, 5.3, and above
we discovered how we could use
debug.getupvalue to implement
setfenv. For dynamic
scoping we'll rely on the
The signature of
debug.getlocal ([thread,] level, local). In
this example we're not concerned with the thread so we'll focus on
levelis an integer that represents how many levels up the call stack we want to look for the variable we're searching for.
localis the index of that local variable we want to resolve, starting at 1.
The return value of this function is either
nil if nothing was found, or the
name and value of the variable.
To find a local variable in an higher up scope, we just need to keep
level and querying each local variable by its numeric index
until we find the matching name. Here’s the implementation:
function dynamic(name) local level = 2 -- iterate over while true do local i = 1 -- iterate over each local by index while true do local found_name, found_val = debug.getlocal(level, i) if not found_name then break end if found_name == name then return found_val end i = i + 1 end level = level + 1 end end
Now we can rewrite the example from the top of the post to use this function:
local function make_printer() local a = 100 return function() print("Lexical scoping:", a) -- notice we pass in "a" here print("Dynamic scoping:", dynamic("a")) end end local function run_func(fn) local a = 200 fn() end local print_a = make_printer() run_func(print_a)
When to use dynamic scoping
In the general case, it’s probably best to avoid dynamic scoping since it makes code harder to understand at a glance. In any case, there are some situations where dynamic scoping is useful.
DSLs, where terseness is important, can
benefit from dynamic scoping by using the implicit context of function calls to
make arguments available that haven’t been explicitly passed. A basic example
would be removing the need to pass
self as an argument if it can be fetched
from the containing scope.