Let's Bring Back JavaScript's `With()` Statement
macarthur.me
macarthur.me
For semantic impact: it's not just walking the prototype chain, it's that the look up is 100% dynamic which means the lookups can change midway through execution, e.g.
let thing = ....;
let count = ....;
with (o) {
for (let j = 0; j < count; j++) {
thing();
}
}
Now lets imagine `thing()` adds a property named `thing` or `count` to anything in `o` or its prototype chain then the lookup of count or thing changes. It is _not_ a bunch of additions to the scope chain at the entry to the `with` block, it makes every identifier lookup that isn't lexically scoped inside the `with` (e.g. only `let` lookups inside their defining local scope). Will repeatedly attempt o.<identifier> every single time, and adding or removing those properties to `o` directly or indirectly during execution will change how even local variables are shadowed while inside the `with`, even in "static" loops.This brings about the performance overhead: without a with block looking up a local variable (var, let, arguments) is generic a single load, even looking up a global variable is generally just a single additional indirect load. Once a single `with` is involved the best case is that every local variable access requires multiple indirect loads and a comparison to validate that the normal load path is involved. That's the ideal case where the property cache hits 100% of the time.
The semantic issues make with() a foot gun, essentially it means that adding new APIs that touch the standard Object prototype chain become even harder, it makes code more fragile in the face of other code changes, and the fragility is subtle and can't be statically detected and isn't a 100% failure case. That's why it was deprecated from JS. Other languages that support with() or equivalent (e.g. pascal family) have static typing so the identifier resolution is (1) static so doesn't impact performance and (2) can warn about shadowing.
From an engine point of view there's an immense amount of complexity of trying to get remotely decent performance of code inside `with` blocks (and it would still be an order of magnitude slower than non-with code), that would be to support a feature that wasn't heavily used even before the modern era of JS, and has negligible use now.
Dynamically scoped variables are a much better solution to global variables… so for example, in Common Lisp you can have a STANDARD-OUTPUT variable that you can use to log things in your code, and then if you want to temporarily redirect your logs to a file, you can just do:
(let ((*STANDARD-OUTPUT* file))
(your-code-runs-here))
And the code in the block will automatically redirect your output to the file.From what I can tell, your CL example is truly dynamically scoped (assuming the global is defined with `defvar`)—functions defined outside of that `let` form will still use the version you give in the `let` when they're called inside that call tree.
Where JavaScript does have a bit of dynamic scoping is with `this`. All OOP languages technically use dynamic scope for `this`, but JavaScript even more so since `this` can be rebound arbitrarily.
val fullName = with(miltonFriedman) {
"$firstName $lastName" <-- slick!
}
That's a famously poor idea; these designers must have been playing hookie when the issue with the Pascal WITH statement was discussed in compiler class.Yeah, it's slick allright---like an oil spill in a hairpin turn.
When we bring into various scopes throughout a program all of the members of an object, we allow the definition of that object, from a distance, to manipulate those scopes.
with (thisObj) {
with (thatObj) {
fun(foo, bar, xyzzy);
}
}
Today, foo and bar come from thatObj, whereas xyzzy comes from thisObj.Tomorrow, thatObj changes and suddenly has a xyzzy member, so xyzzy now refers to the one in thatObj. You have a bug which is distant from the definition of thatObj.
The bad WITH is still found in Modula 2, but the late Niklaus Wirth dropped it when designing Oberon.
https://www.modula2.org/reference/withstatements.php
Oberon-2 has a WITH statement that is something else entirely; some kind of switch that dispatches branches of code based on the dynamic type of an expression, such that the value is then considered to have a certain static type in each of the branches.
Modula-3, dsigned by Luca Cardelli and a few other authors, has a sanely-designed WITH statement:
https://www.cs.purdue.edu/homes/hosking/m3/reference/with.ht...
This has the property that all the identifiers it introduces have to be explicitly mentioned. It's like a kind of aliasing let. Not exactly like symbol-macrolet in Common Lisp, but similar.
When a construct does implicitly bind identifiers, they should be fixed, documented ones: e.g. anaphoric if macro binding the it variable, or loops binding a break or continue.
Constructs that create a wormhole whereby a distant part of the program elsewhere controls the content invite bugs. It's like the scoping equivalent of the COME FROM statement in Intercal.