Hacker News new | ask | show | jobs
by zwieback 2100 days ago
I never understood the advantages of dynamic scoping. It always seems to just boil down to a worse global or thread-local variable.

Is there a simple real-world example that would explain when dynamic scoping would be better than some kind of access protocol to a shared value?

3 comments

One of the key tenets of software engineering is encapsulation: minimizing the number of parts of the program that need to care about X for any given X. Languages have lots of different ways to encapsulate different kinds of stuff from different kinds of code. Local variables encapsulate variables from other functions. Interfaces encapsulate method bodies from invocations. OOP encapsulates state from operations that modify it. You get the idea.

One pattern that most languages don't support encapsulating is this: Say you have a() which calls b() which calls c() which calls d(). d() needs to get some data from a(). The typical way to handle that is by passing that data as parameters through b() and c(), but that couples those middle-level functions to a() and d(). Any time you change the data a() needs to get to d(), you have to touch b() and c() too.

You could wrap the data in some opaque "context" parameter and pass that through b() and c(). That's an OK solution and is pretty common. But a() still has to opt in to that pattern, which means b() and c() are still coupled to the choice to use any encapsulation at all.

Dynamic scoping is a solution to this. a() can bind a value to a dynamic variable and d() can access it without it having to pass through b() and c() at all. It essentially gives you a side channel for parameters.

A more concrete example is trees of UI components. Pretty often you have some big top level UI component that has a lot of application-level business state. Down in the leaves, you have UI components specific to that application that need that state and render it. But in between those you have a bunch of generic UI components like list views, frames, tabs views, radio button groups, that have nothing to do with your app and just visually arrange the UI.

You really don't want to make a new frame widget class every time you need to pass a bit of business data through it into the thing inside the frame. So instead, what a lot of UI frameworks do is support dependency injection. A widget at the root of the tree can provide an object of some type, which makes it implicitly available to all child widgets (transivitely) of that widget. Children far down in the tree can request the object without widgets along that having to pass it along explicitly.

Dependency injection is essentially a re-invention of dynamic scoping.

Ok, that makes perfect sense. To me dynamic scoping was very specifically a language mechanism. What you're describing is more of a framework mechanism and something that would be easier to make easy to understand.
Right, UI frameworks like Angular do dependency injection at the framework level because the underlying language doesn't have direct support for dynamic scope.

Sort of like how you can do object-oriented programming in C by making a struct of function pointers to create your own v-tables.

> Is there a simple real-world example that would explain when dynamic scoping would be better than some kind of access protocol to a shared value?

Safely intercepting global IO.

The average language is never going to thread IO explicitly, so if your callee has not added explicit hook points then all you can do is try to swap out the relevant subsystem, but your average standard IO is usually not even thread-local, and when it is that doesn't help when the language has sub-tread stack swapping (e.g. Python's generator will just suspend the stack relevant section of the stack entirely so if you've updated a threadlocal in a coroutine it is not rolled back on suspension). Plus you still need to remember to properly clean up your threadlocals as they won't self-revert.

With dynamic variables you can just rebind the variable. Only stack frames following yours will see the update, other stacks will be unmolested and none the wiser regardless of your shenanigans.

It's a stack-local global variable.

Here's one case where I wished I had it. I was writing a tool (in Python) for some scientific task. Parse a file, do some calculations, call out to some library to do more calculations. The whole thing was pretty complex, but cleanly architectured. But then the requirements changed. I had to do something different in the innermost function, depending on the configuration. This was probably 6 layers of functions deep.

Now I had two options: add another parameter to every function to carry my configuration variable, or put everything in a class and use a member field. I couldn't use global variables, since I was doing many of these calculations concurrently. And I didn't want to add new parameters, since it clutters the code, and it mixes different levels of abstraction. Most of the intermediate functions don't care about what's going on at the lowest level. Yes it changes their output, so from strictly "functional" best practices I should string along a parameter. But it felt wrong anyway. So what I did was cram everything in a class and call it a day.

With dynamic scoping, I could have put the configuration in a dynamically scoped variable.

Ideally, there would be a way to specify that a function takes dynamic scope. Then tooling would understand that all the intermediate functions have a controlled amount of impurity. In pseudocode:

    MyResult myCalculation(float mass, float energy) (dynamic string extratext) {
        // do the calculation and add extra text to the result
    }

    // way up the call stack:
    using dynamic extratext = "Preliminary, do not publish" {
        calculateAllTheThings();
    }
    
I know this goes against the current trends (make functions pure if possible, avoid mutable state, think a certain way about data flow...). But in practice, those trends sometimes work fine, and sometimes produce convoluted code. In some cases, I find it easier to produce code that looks clean and functional from a domain logic POV, and add stuff that is orthogonal to it (logging, presentation, ...) via a different mechanism.