[proposal] Persistent variables in functions/methods

# Neek Sandhu (6 years ago)

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 properties

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;
    }
})()
# Jacob Pratt (6 years ago)

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

# Neek Sandhu (6 years ago)

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 */)
# Jacob Pratt (6 years ago)

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

# Ben Wiley (6 years ago)

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:

  1. I'm wondering if there's a strong argument for using the word persist rather than static 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.
  2. 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.
  3. Many folks in C++ land seem to think static variables are overused and abused. Do we think the benefits outweigh the pitfalls here?
  4. Are we certain JS engines can't/don't perform the optimization that we would get from static const variables?
  5. 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.
  6. 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:

  1. static is better than persist unless we really think people won't understand how it's different than static class members
  2. disallow unless there's a clearly good use case
  3. not sure
  4. no idea
  5. 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.
  6. 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 :

# Neek Sandhu (6 years ago)

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

# Peter Jaszkowiak (6 years ago)

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

# Neek Sandhu (6 years ago)

re at Ben

  1. persist because didn’t want to bring in the dark connotation associated with static variables in C++. Although persist is just an example to prove a point, could be anything really (even static 😊)
  2. 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

  1. Ref #2
  2. Umm…. Lets just say the fact we need to preserve state (like counter or lastValue) is good enough selling point
  3. This proposal aims for and enforces, once again, the motto that stuff should “live” where it “belongs” and not “leak” out unnecessarily
  4. Ref #5 and also see my reply to jhpratt that explains how this fails when class methods are in question (see DBService class example)
# Isiah Meadows (6 years ago)

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:

  1. 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.
  2. 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:

  1. Break it into a separate module.
  2. Create a class for the mess.
  3. Create an object for the mess.
  4. 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

# Ben Wiley (6 years ago)

@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 :

# T.J. Crowder (6 years ago)

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

# Naveen Chawla (6 years ago)

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.

# Neek Sandhu (6 years ago)

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

# Neek Sandhu (6 years ago)

@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

# Neek Sandhu (6 years ago)

@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

# Herbert Vojčík (6 years ago)

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 properties

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;
 � � }
})()

If we get do expressions, we can, afaict, simply do

const foo = do { let counter = 0; const httpRE = /^https?/; n => { counter++; return n * a; } };

# T.J. Crowder (6 years ago)

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

# Ranando King (6 years ago)

Even if we don't get do expressions, the proposal I've recently submitted includes this ability as a side effect.

rdking/proposal-object-members/blob/master/README.md