Dynamic scoping in Lua
What is dyanmic scoping An example Implementing dynamic scoping When to use dynamic scoping
What is dyanmic 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 print_a because 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 is 200.
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 debug library. We’ll mimic the example above with a function called dynamic that 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 debug.getlocal function.
The signature of getlocal is debug.getlocal ([thread,] level, local). In this example we’re not concerned with the thread so we’ll focus on level and local.
level is an integer that represents how many levels up the call stack we want to look for the variable we’re searching for. local is 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 incrementing 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.