skip to content

A helper reads a page-width setting it never declares: under lexical scope versus dynamic scope, which declaration wins?

level: middleimportance: must knowfreq 55%

answer

  1. the text, or the call chain
  2. decided before running, or during
  3. a caller cannot change a lexical binding
  4. a free name becomes an implicit parameter
  5. most recent active declaration wins

basics

~20 s

Under lexical scope the read binds to the declaration in the surrounding program text, decided before the program runs. Under dynamic scope it binds to the most recent still-active declaration on the chain of calls, so the caller decides.

solid answer

~50 s

The helper mentions a *free* name - one it does not declare - so some other declaration has to supply it, and the two rules look in different places. **Lexical scope** searches the blocks that textually enclose the helper, outward to the first declaration; the answer is the same on every call, and reading the helper plus its surrounding text is enough to know it. **Dynamic scope** searches the calls that are currently active, from the most recent outward, so a caller that declared its own page width hands that value to the helper without either of them saying so. Lexical resolution can be settled before the program runs; dynamic resolution can only be settled while it runs, and the same helper can read a different width on each call depending on who called it.

code

pseudocode · 9 lines
pseudocode
declare width = 80

function render_row(cells)
    return pad(cells, width)   // 'width' is free in this body

function render_narrow(rows)
    declare width = 40
    for each row in rows
        emit(render_row(row))  // lexical: pads to 80 | dynamic: pads to 40

go deeper

for a junior

Know that a name a routine does not declare has to come from somewhere, and that the usual rule is the surrounding program text rather than whoever happened to call it.

for a middle

Describe both searches precisely - enclosing blocks versus active calls - and say when each is decided. Work a two-declaration example and give the value each rule produces.

for a senior

Explain the maintenance cost of caller-supplied bindings: an ambient setting left bound after an unusual exit, or two unrelated modules coupled by sharing a name.

for a principal

Rule on where ambient context is allowed at all, since every such binding is an undocumented parameter that the type system and the reviewer both miss.

## What a free name is A name mentioned in a routine body that the body does not declare is a **free** name. Something outside has to supply its binding, and the scoping rule is the policy that says what "outside" means. There are two answers, and they are not two spellings of one idea - they search different structures. - **Lexical (static) scope** searches the *program text*: the blocks that enclose the mention, outward until one of them declares the name. - **Dynamic scope** searches the *call chain*: the invocations still in progress when the mention is reached, from the most recent one outward, until one of them has a live declaration of the name. ## Walking the renderer example A top-level declaration sets a page width. A row renderer mentions that width but declares nothing. A narrow-table routine declares a page width of its own and then calls the row renderer in a loop. - Under **lexical** resolution, the row renderer's mention was bound when the program was written: it reaches outward through the text, finds the top-level declaration, and pads to the top-level width on every call. The narrow-table routine's declaration is invisible to it, because that declaration is not in the text enclosing it. - Under **dynamic** resolution, the mention is resolved when it is reached: the most recent active declaration belongs to the narrow-table routine, so the rows come out at its width. Called from somewhere else, the very same routine produces a different width. Neither is "wrong" - they are different contracts. The second one is what people mean by a setting a callee inherits from whoever called it. ## The comparison an interviewer wants | | lexical scope | dynamic scope | |---|---|---| | what is searched | enclosing blocks in the text | calls still active at that moment | | when it is decided | before the run, from the text | at the moment of the mention | | who can change the answer | whoever edits the enclosing text | whichever caller is on the chain | | what a reader must have | the routine and its enclosing text | every possible call path into it | | a free name behaves like | a reference to a known declaration | an argument nobody wrote down | | effect of renaming a binding | local and checkable | can silently break distant callees | ## Why one of them became the default The deciding argument is **local reasoning**. Under lexical resolution, the meaning of a routine is a function of the text around it, so you can read it, rename inside it, inline it, or move it with the enclosing text and know what you changed. Under dynamic resolution, a free name is an **implicit parameter** that no signature documents: to know what a routine does you must know every path that can reach it, and any caller anywhere can change its behaviour by declaring a name it happens to share. That is also the failure mode - a name collision between two unrelated parts of a program becomes a behavioural coupling. What dynamic resolution buys is the flip side of the same property: a setting can be established once, for the duration of a call, and every routine underneath it picks the setting up without threading a parameter through layers that do not care. That convenience is real, which is why the pattern survives in a controlled form. ## Getting the convenience without the rule Where the ambient behaviour is genuinely wanted, the modern shape is to make it explicit rather than to make the whole language dynamic: 1. Declare a context object that holds the setting. 2. Bind it for the extent of a call, and restore the previous binding when that call exits, whatever exit it takes. 3. Have routines read it through a named accessor, so the dependency is visible in the code that uses it. That is dynamic resolution in semantics - the value comes from the caller, the most recent binding wins, and it is unbound on exit - with the difference that the dependency is written down and searchable. A candidate who names this connection is demonstrating that they understand the mechanism and not just the two labels. ## Common errors - Stating the axis backwards, as if lexical resolution happened at run time and dynamic resolution at build time; lexical is decided from the text before the run, dynamic only while it runs. - Saying dynamic resolution looks at the *caller's text*. It does not: it looks at the *active calls*, which may be many frames deep and may differ from one call to the next. - Confusing dynamic scope with choosing an implementation from a value's run-time type. Both are decided at run time and they are otherwise unrelated: one resolves a name, the other selects an operation. - Assuming the two rules always agree. They agree whenever only one declaration of the name is in play, which is why the difference can hide for a long time and then surprise.

  • Why did lexical resolution become the default rule?
    Local reasoning. With lexical resolution the meaning of a routine follows from the text around it, so reading, renaming, inlining and moving it are checkable operations. Dynamic resolution turns every free name into an argument no signature records, so understanding one routine means knowing every path that can reach it.
  • How would you get the caller-supplies-it behaviour deliberately, in a lexically scoped program?
    Bind an explicit context value for the extent of a call and restore the previous binding on every exit from that call, then read it through a named accessor. The semantics match dynamic resolution - the nearest active binding wins and it is unbound on exit - but the dependency is written down and can be searched for.

saying these in an interview costs you the question

  • Says lexical resolution happens at run time and dynamic at build time
  • Thinks dynamic scope reads the caller's text rather than the active calls
  • Confuses dynamic scope with selecting an operation from a run-time type
  • Claims a free name can be understood from its own body under dynamic scope
  • Believes the two rules always resolve to the same declaration