skip to content

Lexical vs Dynamic Scope

Where a name inside a function is looked up: the surrounding text, or whoever called it. Interviewers ask because the answer is what makes a captured variable predictable at all.

on this pageshow

questions

5

Under lexical scoping, where is a free name inside a message-formatting helper looked up?

level: juniorimportance: must knowfreq 72%

answer

  1. read outward, not upward
  2. the text around the definition
  3. innermost enclosing region wins first
  4. the call site is not searched
  5. decidable without running the program

basics

~20 s

Lexical scoping resolves a free name in the text surrounding the helper's definition - innermost enclosing region first, then outward. The call site is never consulted, so the helper means the same thing everywhere it is used.

solid answer

~50 s

A **free name** in a function body is one the body neither declares nor receives as a parameter. Under lexical (static) scoping, that name is resolved by walking outward through the regions of program text that enclose the *definition*: the enclosing block, then the enclosing function, then the enclosing module or file, then the outermost region. The first declaration found wins; if none is found, the name is unresolved and reported as an error. Crucially, the regions searched are the ones that enclose where the helper was *written*, not where it is *called* - a module that calls the helper can declare a local of the same name and the helper will not see it. That is why you can decide what a name refers to by reading the program rather than by running it.

code

pseudocode · 10 lines
pseudocode
// module A - where the helper is written
let prefix = '[app]'

function format(message)
    return prefix + ' ' + message      // prefix is free in this body

// module B - a different caller, with a local of the same name
function report()
    let prefix = '[report]'
    return format('disk full')         // yields '[app] disk full'

go deeper

for a junior

Be able to point at a body and say which names are free, then say the search goes outward through the text around the definition. That sentence alone answers the first-screen version of this question.

for a middle

Explain the search order as a chain of enclosing regions with first-match-wins, and be ready to say what happens when nothing declares the name and when a parameter shares a name with an outer declaration.

for a senior

Show why this rule makes a helper reviewable on its own: its inputs are its parameters plus the text around it, and no call site can inject a different meaning. Use that when arguing about where a helper should live.

for a principal

Frame the rule as a trade: explicitness bought at the price of threading caller-chosen values through as parameters. Decide deliberately when a team may install an ambient value instead, and make that the exception you can name.

## What makes a name free Every name mentioned in a function body falls into one of three groups: - **Parameters** - bound by the function's own signature. - **Locals** - declared inside the body itself. - **Free names** - everything else the body mentions and does not bind. Only the third group needs a scoping rule. A message-formatting helper that reads `return prefix + ' ' + message` binds `message` as a parameter and leaves `prefix` free. Something outside the body has to say what `prefix` means, and the scoping rule is precisely the answer to *what*. ## The scope chain, walked outward from the definition Under lexical scoping the search proceeds through the nested regions of text that surround the point where the function was written: 1. The innermost block containing the reference. 2. Each enclosing block, out to the body of the function itself. 3. The body of any function that lexically *contains* this one. 4. The enclosing module, file or compilation unit. 5. The outermost, program-wide region. The first region that declares the name wins, and the search stops there. If no region declares it, the name is unresolved - some languages report that before the program runs, others only when the line executes, but in both cases no caller can supply the missing declaration. The important property of this list is that it is a property of the **text**, not of the run. Nesting is fixed the moment the code is written. So the question "which declaration does this name refer to?" is answerable by a reader with a copy of the source and no debugger, and the answer cannot change between two executions of the same program. ## The alternative: asking whoever called The competing rule, **dynamic scoping**, resolves a free name by searching the chain of calls that are active at that moment: the calling function's bindings, then its caller's, and so on outward, with the innermost *active* binding winning. The same helper text then means different things depending on the path that reached it. | Question | Lexical scoping | Dynamic scoping | |---|---|---| | What is searched? | the text enclosing the definition | the chain of active calls | | When can it be decided? | by reading the program | only while the program runs | | Can a caller's local interfere? | no | yes, it can override or supply the name | | Does moving the call change the answer? | no | yes | | What if nothing declares the name? | an unresolved name, found statically or on that line | depends on which caller happens to be on the chain | Most mainstream languages settled on lexical scoping as the default, and the ones that historically defaulted to caller-driven lookup have mostly moved to lexical defaults with caller-driven bindings left as an explicit, opt-in facility. ## Why the lexical answer is the one closures need A closure is a function packaged with the surrounding bindings it refers to. That package is only meaningful because lexical scoping fixes **which** declaration each free name refers to at the moment the function is written. If the rule were caller-driven, there would be nothing stable to package: the free names would be resolved fresh, against a different chain of calls, on every invocation, and a function returned from the code that created its environment would carry nothing useful with it. So the honest statement is not "closures exist, and by the way names resolve lexically". It is the other way round: lexical resolution is the thing that makes the captured environment well-defined at all. ## Reading it in practice Two habits follow directly: - **To understand a helper, read outward from the helper.** Open the file it lives in and look at the enclosing text. Do not go hunting through its callers for the meaning of a name. - **To change what a helper sees, change the text around it or pass a parameter.** Declaring a same-named local in the caller does nothing; that is not a limitation to work around but the guarantee that makes the helper reviewable in isolation. The cost of the rule is that implicit, caller-supplied inputs are not available by default - if a helper needs a value the caller chooses, that value must be threaded in as a parameter or captured from the text around the definition. That explicitness is the whole trade, and it is why the lexical answer is the default nearly everywhere.

  • What happens if no enclosing region declares the free name at all?
    The name is unresolved and the program is wrong. Some languages report it before running, others only when that line executes, but in neither case can a caller supply the missing declaration - the search only ever looks at text enclosing the definition.
  • If the helper is moved into a different file, can its behaviour change?
    Yes, because moving the text moves the enclosing regions the search walks. A free name that resolved to one declaration in the old surroundings may resolve to a different one, or become unresolved, in the new ones. That is why moving a helper is not always a pure refactoring.
  • Does a parameter with the same name as an enclosing declaration cause a conflict?
    No. The parameter is bound by the function itself, so it is found first and the enclosing declaration is simply not reached from inside that body. The outer declaration is untouched and still visible to code outside the function.

A form that says "use the address printed above" means the address on its own page. Carrying the form to a different desk does not change which address it points at.

saying these in an interview costs you the question

  • Says the helper can see the calling module's local variables.
  • Thinks a free name is resolved fresh at each call site.
  • Believes the most recently assigned value anywhere wins.
  • Treats an enclosing declaration as invisible unless passed as a parameter.
  • Cannot say which names in a body are free.
open as a page

A shared formatting helper produces different output depending on which module calls it - which scoping rule explains that?

level: middleimportance: must knowfreq 58%

basics

~20 s

Caller-driven, or dynamic, scoping explains it: a free name in the helper is resolved against the chain of calls active at that moment, so each caller's own binding of that name supplies a different value.

open as a page

When an inner declaration reuses an enclosing name, which binding does the scope chain give the inner body?

level: middleimportance: should knowfreq 46%

basics

~10 s

The innermost declaration wins inside its own region: the outward search stops at the first one it meets. The enclosing binding is hidden there, not destroyed, and code outside that region still sees it.

open as a page

Formatting functions are registered at startup and invoked later from unrelated call sites - what does lexical resolution guarantee about their free names?

level: seniorimportance: should knowfreq 38%

basics

~20 s

It guarantees that each free name still refers to the declaration the text around the definition chose. Deferral, storage and an unfamiliar call site cannot re-point a name, so a registered function can be reviewed where it was written.

open as a page

Dynamic scoping is a liability by default, so where does caller-determined name lookup still earn its place?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

It earns its place for values every layer would otherwise thread through untouched - a formatting locale, an output sink, a verbosity setting. Modern languages offer this as an explicit, named, opt-in binding rather than the rule for all free names.

open as a page