[proposal] Persistent variables in functions/methods
Can you explain the difference between this and static properties? At quick glance, I don't see any.
Would this not work?
function foo(n) {
this.counter++;
return n * a;
}
foo.counter = 0;
foo.httpRE = /^https?/;
jhpratt
It’d work but won’t be beautiful. With this proposal, code would be much easier to reason about.
Also it breaks encapsulation (logical, semantic and syntactic), stuff should live where it belongs. If it “belongs” inside a function, then it should “live” inside function body { }
In the same context, notice how we evolved from callbacks to Promises and now to async/await shorthands.
const res = await fetch('http://example.com')
More pretty than
fetch('http://example.com')
.then(res => /* impl */)
If it “belongs” inside a function, then it should “live” inside function
body { }
Agreed, but these values don't live inside the function, but rather alongside the function. If it were inside, it would share the lifetime with the function call (which it doesn't).
jhpratt
I generally think JavaScript is a much better looking language than C++ but this is one of my favorite C++ features, so it's interesting to see it considered for JavaScript.
A few questions:
- I'm wondering if there's a strong argument for using the word
persist
rather thanstatic
as C++ does. Certainly this isn't the same thing as a static class member but they're not dissimilar concepts, and use areas are different enough to avoid language ambiguity. - Does the static initialization have access to non-static variables,
function parameters or
this
members? Might be less error prone if not, but maybe they are compelling use cases for that. - Many folks in C++ land seem to think static variables are overused and abused. Do we think the benefits outweigh the pitfalls here?
- Are we certain JS engines can't/don't perform the optimization that we would get from static const variables?
- Doesn't module level scope in es2015+ give us basically the same benefit we would get for static const vars in functions? Sure it's not declared in the same place, but it's still well encapsulated and not available off the object or class itself.
- Is there a great reason for static let rather than defining the "static" variable in an outer closure per your code example?
My own tentative answers are:
- static is better than persist unless we really think people won't understand how it's different than static class members
- disallow unless there's a clearly good use case
- not sure
- no idea
- I think I always use module scope in JS where I would use static const in C++. Seems pretty good even if it reads a bit differently. Arguably better because variable lifetime is apparent based on indentation.
- I think there's an argument here for conciseness.. other than that I'm not sure what the reason would be (unless you have a good use case for #2).
Ben
Le mar. 17 juill. 2018 00 h 56, Neek Sandhu <neek.sandhu at outlook.com> a écrit :
RE at jhpratt
Well that’s the proposal. In case there’s a confusion with “lifetime”. What I meant was that as far as garbage collector is concerned, persistent variables live as long as the function is referenced somewhere (unlike anonymous functions)
Also revisiting your idea of using static properties instead, how’d you imagine static props on methods of classes considering the fact that stuff should “live” where it “belongs” and not “leak” out unnecessarily, imagine this:
class DBService {
foo(uri) {
persist const httpRE = /^https?/;
persist let counter = 0;
return 0;
}
In the example above it is given that no other method in DBService
class uses, or has anything to with counter
“inside” of DBService#foo
. So “leaking” it out is unnecessary
This is just syntactic sugar for existing functionality. It is only useful for a single use case, one which is generally rare anyways, and easily accomplished when necessary. It provides like to no benefit in clarity, concision, or flexibility over existing solutions. It adds another way of storing state for methods, which is introducing unnecessary ambiguity over where that state should be stored (as a member of the parent object).
It's also worth mentioning that a more general solution most likely exists in the proposed addition of decorators.
This proposal is yet another example of niche nice-to-have turned into generally useless syntax proposal.
On Jul 16, 2018 23:30, "Neek Sandhu" <neek.sandhu at outlook.com> wrote:
RE at jhpratt
Well that’s the proposal. In case there’s a confusion with “lifetime”. What I meant was that as far as garbage collector is concerned, persistent variables live as long as the function is referenced somewhere (unlike anonymous functions)
Also revisiting your idea of using static properties instead, how’d you imagine static props on methods of classes considering the fact that stuff should “live” where it “belongs” and not “leak” out unnecessarily, imagine this:
class DBService {
foo(uri) {
persist const httpRE = /^https?/;
persist let counter = 0;
return 0;
}
In the example above it is given that no other method in DBService
class
uses, or has anything to with counter
“inside” of DBService#foo
. So
“leaking” it out is unnecessary
re at Ben
persist
because didn’t want to bring in the dark connotation associated withstatic
variables in C++. Althoughpersist
is just an example to prove a point, could be anything really (evenstatic
😊)- As for my knowledge and understanding, I’d imagine these to behave just like closures w/ IIFE
function foo(n) {
// initialized on first call to foo, persists for subsequent calls
persist let counter = 0;
// this way JS engine won't have to recompile the pattern everytime
persist const httpRE = /^https?/;
counter++;
return n * a;
}
is 1:1 as
let foo = (() => {
let counter = 0;
const httpRE = /^https?/;
return (n) => {
counter++;
return n * a;
}
})()
Revisions to this are welcome of course
- Ref #2
- Umm…. Lets just say the fact we need to preserve state (like
counter
orlastValue
) is good enough selling point - This proposal aims for and enforces, once again, the motto that stuff should “live” where it “belongs” and not “leak” out unnecessarily
- Ref #5 and also see my reply to jhpratt that explains how this fails when class methods are in question (see
DBService
class example)
Well that’s the proposal. In case there’s a confusion with “lifetime”. What I meant was that as far as garbage collector is concerned, persistent variables live as long as the function is referenced somewhere (unlike anonymous functions)
Who says an implementation is actually going to adhere to that? As far as I understand, engines try to merge as many closures as they can, so this, for example, only forms one closure:
function foo() {
var counter = 0
return {
inc() { counter++ },
get() { return counter },
}
}
I would expect engines to probably do similar with persistent variables, just they would probably create a separate closure if it references nothing else in the immediate parent closure. But unless you do that for almost all your closure variables, I'm not sure you'd see any real wins here. On top of that, there's two other issues:
- GC is usually not a performance issue unless you're dealing with a lot of objects. You won't likely ever hit this unless you're doing a virtual DOM implementation, a real-time video game, or something similarly computationally demanding.
- I have no shortage of criticisms over how poorly engines optimize
closures compared to objects. Closures should be simpler and easier to
optimize (it's a function body + phantom
this
, but you know the shape of it statically), yet somehow they aren't seeing very many of the optimizations performed on objects? Because of this, I also don't see static variables out-performing object properties.
Separately, I don't really see the benefit of this apart from scoping and simple caching, and most of the time when it could've been useful, it's been more useful for me to do one of the following:
- Break it into a separate module.
- Create a class for the mess.
- Create an object for the mess.
- Some combination of the above.
Each of these helps mitigate the scoping issue, and some of them also help ease the caching issue by keeping the cache closer to where it's used.
In my experience, when you start having much of a need of local static variables, it's usually a good time to consider refactoring the code so it uses a more structured data representation. It's usually not hard to restructure it, but it results in code that's much more predictable and easier to understand. The data model usually ends up clearer, and it's easier to optimize it. Sometimes, you end up simplifying, fixing, or optimizing it because when you better centralize and structure the data types, it becomes easier to spot things that seem out of place.
Isiah Meadows me at isiahmeadows.com, www.isiahmeadows.com
@peter: I think you made some valid point in there but you could have been more concise and nicer :) www.destroyallsoftware.com/blog/2018/a-case-study-in-not-being-a-jerk-in-open-source
@Neek: worth noting that your compiled example is not semantically identical to the proposed behavior. In the closure example the persistent variables are initialized at the time the inner function is defined, not at the time it is first called. If it's initialized at the time of function definition then there's no persist access to local variables inside the function itself (although different than C++, not a bad thing imo!).
Ben
Le mar. 17 juill. 2018 01 h 51, Neek Sandhu <neek.sandhu at outlook.com> a écrit :
On Tue, Jul 17, 2018 at 6:06 AM, Jacob Pratt <jhprattdev at gmail.com> wrote:
Would this not work?
function foo(n) { this.counter++; return n * a; } foo.counter = 0; foo.httpRE = /^https?/;
Did you mean foo.counter++;
? Because to make this
inside a call to
foo
be foo
itself, you'd have to call foo
like this: foo.call(foo, 42)
. counter
and httpRE
are also fully exposed. Not desirable IMHO.
On Tue, Jul 17, 2018 at 6:51 AM, Neek Sandhu <neek.sandhu at outlook.com>
wrote:
is 1:1 as
let foo = (() => { let counter = 0; const httpRE = /^https?/; return (n) => { counter++; return n * a; } })()
That isn't 1:1 with what I understand you're propoing. counter
and
httpRE
are initialized when foo
is created, not when it's first called.
(IIRC, this is true of C's static
variables, too, but it's been ~25
years...)
Another existing pattern to compare with (which also initializes them up-front, not on first call):
let foo;
{
let counter = 0;
const httpRE = /^https?/;
foo = (n) => {
counter++;
return n * a;
};
}
You can defer init by rewriting foo
, but I really don't like that
idea, not least because anything grabbing foo
before calling it keeps the
wrong one):
function foo(n) {
let counter = 0;
const httpRE = /^https?/;
foo = (n) => {
counter++;
return n * a;
};
return foo(n);
}
Again, I really don't like that, but it does defer init.
Given how easy this is with block scope or, as Ben Wiley points out, module scope, I think there would need to be a stronger use case.
-- T.J. Crowder
Seems attractive at first, but the problem for me is readability. It seems to me that we are trained to understand variable assignments as happening where they are written, e.g.
persist let counter = 0;
counter++;
With the "persist" feature, the value of counter after counter++
could be
67, or 189. However, it seems to me that we are trained to understand that
the value of counter after counter++
would be 1. Because upon glance we
may have expected the let declaration to be executed each time where it is
written in the function.
Ok maybe you guys are not impressed enough 😊
Here’s a 339 line file and on Line 85 there’s a variable called workerInitPromise
Only one method in that class is concerned with workerInitPromise
, that is startWorker()
What is workerInitPromise
?
Well, whenever startWorker()
is called it should return a Promise
that should resolve when the Worker
is up and running.
That means one call to startWorker
has started the worker startup sequence and it’d be wasteful to start it over again when someone else calls startWorker
.
Instead startWorker
decides to “share” the Promise
amongst furious callers. Now, startWorker
needs a place to store that Promise
so he can share with subsequent callers.
Where is that place???
And that my friends is why I had to create workerInitPromise
prop on the class, just to make startWorker
happy.
It “belongs” in startWorker
and should “live” inside startWorker
@Naveen
Can’t be more confusing than this
in JavaScript
IMO it’s better than defining counter
in outer scope. But hey, confirmation bias, can’t help it
@Ben
worth noting that your compiled example is not semantically identical to the proposed behavior
😧 My bad. I’m gonna take that back. But regardless I really don’t see any design/runtime/technical flaw[1] with the compiled behavior
[1] Would love to see one though
Neek Sandhu wrote on 17. 7. 2018 6:56:
It'd be really useful to have variables inside methods/functions that are initialized once, reused for subsequent calls and live as long as containing scope i.e the function itself.
not to be confused with
static
propertiesUse Cases
Almost every app has a function or method that needs to "preserve" some state across multiple calls, a counter for example. Current ways out of this situation are either closures created with IIFE's or making those variables top-level. Both of which are ugly. I think the same could be done much more neatly with persistent variables.
Example
function foo(n) { � � // initialized on first call to foo, persists for subsequent calls � � persist let counter = �0; � � // this way JS engine won't have to recompile the pattern everytime � � persist const httpRE = /^https?/; � � counter++; � � return n * a; }
is 1:1 as
let foo = (() => { � � let counter = 0; � � const httpRE = /^https?/; � � return (n) => { � � � � counter++; � � � � return �n * a; � � } })()
If we get do expressions, we can, afaict, simply do
const foo = do { let counter = 0; const httpRE = /^https?/; n => { counter++; return n * a; } };
On Wed, Jul 25, 2018 at 12:02 PM, Herbert Vojčík <herby at mailbox.sk> wrote:
If we get do expressions, we can, afaict, simply do...
Very good point. It's nicely concise, too.
-- T.J. Crowder
Even if we don't get do expressions, the proposal I've recently submitted includes this ability as a side effect.
It'd be really useful to have variables inside methods/functions that are initialized once, reused for subsequent calls and live as long as containing scope i.e the function itself.
Use Cases
Almost every app has a function or method that needs to "preserve" some state across multiple calls, a counter for example. Current ways out of this situation are either closures created with IIFE's or making those variables top-level. Both of which are ugly. I think the same could be done much more neatly with persistent variables.
Example
function foo(n) { // initialized on first call to foo, persists for subsequent calls persist let counter = 0; // this way JS engine won't have to recompile the pattern everytime persist const httpRE = /^https?/; counter++; return n * a; }
is 1:1 as
let foo = (() => { let counter = 0; const httpRE = /^https?/; return (n) => { counter++; return n * a; } })()