3

Let's say I want to rotate class names for my button on click. Clicked once becomes button-green, twice - button-yellow, thrice - button-red. And then it repeats, so fourth click makes it button-green again.

I know other techniques how to do it, I'm not asking for implementation advice. I made up this example to understand something about generators in JavaScript.

Here's my code with generator:

function* rotator(items) {
    while (true) {
        for (const item of items) {
            yield item;
        }
    }
}

const classRotator = rotator([
    'button-green',
    'button-yellow',
    'button-red',
]);

document.getElementById('my-button').addEventListener('click', event => {
    event.currentTarget.classList.add(classRotator.next().value);
});

It works fine, except it never gets rid of the previous class. The most convenient way would be to read the current state before getting the next one:

// .current is not a thing:
event.currentTarget.classList.remove(classRotator.current);

Of course I can keep this value on my own and use it. Likewise, I can clear all classes I use in the rotator() myself. I can even make my generator function yield both previous and current value:

function* rotator(items) {
    let previous;

    while (true) {
        for (const item of items) {
            yield {
                item,
                previous
            };

            previous = item;
        }
    }
}

And then use it like this:

document.getElementById('my-button').addEventListener('click', event => {
    const {item: className, previous: previousClassName} = classRotator.next().value;

    event.currentTarget.classList.remove(previousClassName);
    event.currentTarget.classList.add(className);
});

But that's not the point - for educational purpose I'm asking this question:

Can I read the current value of generator function in JavaScript? If not, is it to avoid using memory when it's not needed (this value can potentially be very big)?

Robo Robok
  • 21,132
  • 17
  • 68
  • 126
  • 5
    No, you can't. Your generator however can return a structured result with both the previous and new values. – Pointy Nov 06 '21 at 19:26
  • @Pointy is my reasoning about memory saving... reasonable? :D – Robo Robok Nov 06 '21 at 19:27
  • 1
    Well I don't think it has anything to do with memory; the API is designed to be extremely simple and minimal. The generator is free to return gigantic objects if it wants to. – Pointy Nov 06 '21 at 19:28
  • @Pointy of course, but without storing this gigantic value, it gets disposed as soon as it's used, unless user explicitly stores it. I feel like if it wasn't the reason why there's no `.current` (or whatever) is to avoid keeping unnecessary data. But I can be wrong. Adding this small property or method wouldn't hurt otherwise, I guess. – Robo Robok Nov 06 '21 at 19:32
  • 2
    You could wrap your generator function with your own mechanism that stores the last-generated `.next()` value. – Pointy Nov 06 '21 at 19:33
  • @Pointy I updated my question to include yielding `previous` manually. Would you like to add your "No, it's not possible" as an answer? – Robo Robok Nov 06 '21 at 19:40
  • The memory aspect is moot; Javascript doesn't really care about its memory usage. Generators are designed to be as simple as possible. You'd have to wrap this yourself. You could even wrap it in another generator that stores the previous value and returns it along with the next value. By the way, I know this is for educational purposes, you can save the entire class list as a string before adding the first one and just re-set it to that string before adding each class. – Qix - MONICA WAS MISTREATED Nov 06 '21 at 19:43
  • I also highly recommend reading up on the generator documentation on MDN as there's a wealth of really interesting knowledge there. You can see my (ab)use of them (including sending values back _into_ the generators during their execution) at https://GitHub.com/qix-/scaly, for example. – Qix - MONICA WAS MISTREATED Nov 06 '21 at 19:44
  • @Qix-MONICAWASMISTREATED to be honest, this argument about the API being as simple as possible doesn't convince me. JS was never about simple APIs. They usually include handful of useful properties or methods (`.toggle()`?), but you're telling me they'd skip reading current value for simplicity? Hmm, I'm not buying it. – Robo Robok Nov 06 '21 at 19:46
  • @RoboRobok well one way or the other it's a simple evident fact that the "current" value is not maintained by the API. – Pointy Nov 06 '21 at 19:51
  • @Pointy it sure is, I'd like to know why. I'm still on the "memory" team :D – Robo Robok Nov 06 '21 at 19:54
  • @Pointy please proceed to add it as an answer, or if you don't like to, I'll add it for future reference. – Robo Robok Nov 06 '21 at 19:55

2 Answers2

3

Can I read the current value of generator function in JavaScript?

No.

But you can easily implement this yourself if you want to.

If not, is it to avoid using memory when it's not needed?

Maybe. An iterator is supposed to not cling onto the last value it has generated, to allow independent garbage collection and to make it cheap to keep exhausted or not-yet-finished generators around.

But I think the prime reason why there is no .current field available in the design is that it doesn't make sense from a theoretical standpoint: what value would that field have before the iteration started, or after the iterator is exhausted? One could have opted for undefined, but a clean design simply doesn't have the field at all and only returns values if you actually step the iterator.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Hi Bergi, thanks a lot. Do you know any source to back up our theory about generators doing that for memory saving? I'd choose your answer over Pointy's. – Robo Robok Nov 06 '21 at 21:50
  • 1
    @RoboRobok Not directly (and actually I doubt that avoiding memory usage was a prime factor in the decision). But have a look at [all the links and references here](https://stackoverflow.com/q/53894639/1048572) for why this kind of iterator protocol was deemed better than alternative ones. – Bergi Nov 06 '21 at 22:04
2

JavaScript "native" APIs generally are willing to create new objects with wild abandon. Conserving memory is generally not, by any appearances, a fundamental goal of the language committee.

It would be quite simple to create a general facility to wrap the result of invoking a generator in an object that delegates the .next() method to the actual result object, but also saves each returned value as a .current() value (or whatever works for your application). Having a .current() is useful, for such purposes as a lexical analyzer for a programming language. The basic generator API, however, does not make provisions for that.

Pointy
  • 405,095
  • 59
  • 585
  • 614
  • 1
    Thank you! I don't fully agree with the point about memory though. I feel like many modern JS APIs are way more conscious of memory than the older ones. Hence all iterators over arrays etc. Maybe it's not always crucial on the client side (sometimes is), but in Node.js it's a pretty big deal I suppose. – Robo Robok Nov 06 '21 at 20:03
  • @RoboRobok think of all the `Object` APIs, like `Object.keys()`, which create new arrays. Managing allocated memory is pretty much Job 1 for a JavaScript runtime, so creating and destroying objects has to be efficient. I of course was not in the room when the Generator API was decided, but the fact remains that application code can waste vast amounts of memory despite the best efforts of the API designers. – Pointy Nov 06 '21 at 20:06
  • I can imagine an app having multiple generators created storing huge Blobs, to let's say save them to files. If we had this `.current()` way to read them, all these generators would keep their current Blobs. Otherwise, they're disposed. That's how I see it. I didn't understand your last sentence in the comment above. You're saying that it's a waste of time to make APIs more efficient, because bad devs would still trash their memory, or what? :P – Robo Robok Nov 06 '21 at 20:12
  • Well I respect efforts to be efficient, but there's only so much that the platform designers can do. – Pointy Nov 06 '21 at 20:12
  • So are you agreeing with my theory about memory saving or not? Because I'm confused now. – Robo Robok Nov 06 '21 at 20:13
  • @RoboRobok I don't have enough information or insight to either agree or disagree; I don't know what the rationale was for the feature being the way it is. – Pointy Nov 06 '21 at 20:18
  • Okay, thank you for honesty - I truly appreciate it. I'll dig deeper and post here once I know more. – Robo Robok Nov 06 '21 at 20:20
  • @Pointy The `Object` `.keys`, `.values`, and `.entries` methods are considered a mistake in hindsight even by the committee. Had iterators already existed when `Object.keys` was specified, they would have been used there. – Bergi Nov 06 '21 at 21:36
  • 2
    "*JavaScript "native" APIs generally are willing to create new objects with wild abandon*" - yes, and that's fine and by design. Creating new objects (like iterator `.next()` results) is more memory-efficient than mutating old, long-lived objects: the new and short-lived objects are fast to allocate and easy to deallocate, and it is easy to completely optimise them away completely (e.g. in a `for … of` loop). – Bergi Nov 06 '21 at 21:41