3 ms·
Let's Bring Back JavaScript's `With()` Statement
- drfuchs 3y agoCredit where credit is due: Pascal had “with” back in the 1970’s.
- kragen 3y agowhere it doesn't have most of these problems due to static typing
- olliej 3y agoyeah, it cannot be emphasized enough the hazards of with() like constructs in languages with dynamic types and dynamic variable lookup are drastically different. That's why it was a mistake for it to be in JS, and why it was deprecated and blocked in strict mode.
- deleted 3y ago[deleted]
- drewcoo 3y agoIt was in Burroughs Algol in the 60s. Algol was Pascal's parent. WITH was not in all Algols, but Wirth was free to borrow the best ideas. https://en.wikipedia.org/wiki/ALGOL_60 https://en.wikipedia.org/wiki/ALGOL_60
- olliej 3y agoThe problem with this is it tremendously simplifies the issues of with(), and this simplification makes the semantic and performance impacts of with() seem much less bad that they actually are. 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.
- goranmoomin 3y agoWhile I’ve never actually used `with()` that much, I do feel that it’s a lost opportunity b.c. `with()` was basically the only way to get dynamically scoped variables (instead of lexically scoped ones) in JS (and tbh, in other ALGOL-derived languages as well). 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.
- lolinder 3y ago`with` is still lexically scoped—it just adds another scope to the lexical scope chain. The variables are still scoped to the beginning and end of a particular block of code, and if that code calls out to a function that was defined outside of that block the variables don't follow to that called function (unless explicitly passed as arguments). 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.
- kazinator 3y agoIf with defines variables which are not mentioned in it explicitly, and which are controlled by an entirely different part of the program, possibly in another file, we can make the argument that it is not actually lexically scoped.
- jinushaun 3y agoIMHO, the list of criticisms against `with` in the article are legit. Seems like it causes more trouble than it’s worth. I prefer Python-style with (ie, context) over what seems like syntactic sugar to avoid variable destructuring.
- kragen 3y agothese two constructs are completely unrelated except for using the same keyword
- kazinator 3y agoval 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 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.html 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.