[HN Gopher] Why Would Anyone Need JavaScript Generator Functions?
       ___________________________________________________________________
        
       Why Would Anyone Need JavaScript Generator Functions?
        
       Author : lewisjoe
       Score  : 188 points
       Date   : 2022-11-07 11:57 UTC (11 hours ago)
        
 (HTM) web link (jrsinclair.com)
 (TXT) w3m dump (jrsinclair.com)
        
       | ramesh31 wrote:
       | I classify generators along with switch statements and while
       | loops in JS. Perfectly fine to use if you know what you're doing,
       | but generally a code smell that belies further non-idiomatic
       | code.
        
         | myvoiceismypass wrote:
         | Typescript supports exhaustive switches which are pretty
         | powerful, especially when working with proper union types and
         | the like.
        
           | adam_arthur wrote:
           | I find using objects mapped as switchKeyToFunction to be more
           | idiomatic and readable. e.g. shapeToDrawFn[shape] But to each
           | their own
        
             | phs2501 wrote:
             | Will the compiler check that you have an exhaustive set of
             | keys in your definition of `shapeToDrawFn`? (Not a
             | rhetorical question, I don't know.)
        
         | jalino23 wrote:
         | I prefer switch now over longer if else, and find my code to be
         | much simpler. I stopped following most of what clean code
         | recommends such as using polymorphism as a switch case or if
         | else.
        
           | kevinmchugh wrote:
           | Many times, switch is used when an object would suffice
        
             | aastronaut wrote:
             | I'm sorry, but in my decade of JavaScript I never read
             | anything more cryptic than this. What is this supposed to
             | mean?
        
               | ramesh31 wrote:
               | >I'm sorry, but in my decade of JavaScript I never read
               | anything more cryptic than this. What is this supposed to
               | mean?
               | 
               | Switches in JS are just implicit maps.
               | const caseVal = 'case1';            const result = ({
               | 'case1': (()=> 'case1Val'),        'case2': (()=>
               | 'case2Val'),        'case3': (()=> 'case3Val'),       }
               | [caseVal] || (()=> 'default'))()
               | 
               | Has identical performance to a switch case and is far
               | more idiomatic.
        
               | aastronaut wrote:
               | Thanks for the example!
               | 
               | I assume the identical performance just comes from the
               | optimization of the JIT, as allocating objects in the
               | heap seems quite overkill for such a control flow. I only
               | fall back to this when switch/case isn't available in the
               | language, eg. in Python.
               | 
               | Is this a thing in the JS community?
        
               | lolinder wrote:
               | Not OP, but I've often seen cases where the same set of
               | strings or enum values are used in switch statements in
               | disparate parts of the code base. While each switch is
               | supposed to be checking the same set of values, they
               | invariably end up getting out of sync and checking
               | different subsets of the actual possible set of values,
               | leading to bugs.
               | 
               | Some languages support exhaustive switches (typescript
               | has a way to do this), but oftentimes the better solution
               | is to consolidate all of these switches into an object,
               | or a set of objects that share an interface. That way all
               | of the switch statements become function calls or
               | property accesses, and the type checker can warn you at
               | time of creation of the new object if you're not
               | providing all of the functionality the rest of the code
               | will require.
        
               | couchand wrote:
               | And of course, there is no free lunch. You run smack into
               | the expression problem here.
               | 
               | The question is: do you have more places that check your
               | conditions than types of options? Are you more likely to
               | add a new check (and need to verify exhaustiveness) or
               | add a new option (and need to modify every check)?
        
               | svieira wrote:
               | It's not in JavaScript, but see _Unifying Church and
               | State: FP and OOP Together_ for another example of the
               | "switch / named functions" (or "Wadler's Expression
               | Problem") described in a pretty intuitive way:
               | https://www.youtube.com/watch?v=IO5MD62dQbI
        
               | aastronaut wrote:
               | there goes my evening, that's what I like about HN.
               | thanks for the refs!
        
               | kevinmchugh wrote:
               | A switch statement allows you to have different behavior
               | for different inputs, but often all that's needed is
               | different output data.
               | 
               | If you have to map, say, country codes to country names,
               | writing a long switch statement of case "US": name =
               | "United States of America" break
               | 
               | Is going to suck. An object (in js, an associative array
               | more generally) will be simpler.
               | 
               | It's not always quite so obvious as that example.
        
               | aastronaut wrote:
               | Thanks for the example! I guess we consider switch/case
               | in different situations then. I usually make use of
               | switches in simple mappings and switch/case seems more
               | readable and idiomatic to me there:                 type
               | CountryCode =         | "CH"         | "US";
               | function nameFromCountryCode(countryCode: CountryCode):
               | string {         switch (countryCode) {           case
               | "CH": return "Switzerland";           case "US": return
               | "United States of America";           default: //
               | exhaustiveness checking             ((val: never): never
               | => {               throw new Error(`Cannot resolve name
               | for countryCode: "${val}"`);             })(countryCode);
               | }       }
               | 
               | ...instead of...                 type CountryCode =
               | | "CH"         | "US";            function
               | nameFromCountryCode(countryCode: CountryCode): string {
               | return ({           'CH': (() => 'Switzerland'),
               | 'US': (() => 'United States of America'),
               | }[countryCode] || (() => {           throw new
               | Error(`Cannot resolve name for countryCode:
               | "${countryCode}"`);         }))();       }
        
               | [deleted]
        
               | lolinder wrote:
               | Your second example is _not_ how anyone here is
               | recommending using an object to replace a switch. You 've
               | still got the function call, which is now redundant.
               | 
               | Here's how you'd actually do it:                   type
               | CountryCode =           | "CH"           | "US";
               | const countryNames: Record<CountryCode, string> = {
               | "CH": "Switzerland",           "US": "United States of
               | America",         }
               | 
               | Your second example is way more complicated than your
               | first, but this one is even easier to read (at least for
               | me), and still provides all the same functionality and
               | type safety (including exhaustiveness checking).
        
               | mdaniel wrote:
               | I'll bite: why the anonymous function throwing an Error
               | in the first snippet?
        
               | aastronaut wrote:
               | I didn't lay out a bait =)
               | 
               | It would also look more readable to me with a default
               | return value. An exhaustiveness check just keeps your
               | mapping functionally pure and the type checker can catch
               | it.
        
               | aastronaut wrote:
               | @lolinder
               | 
               | ...you just removed the default case and just introduced
               | _undefined_ as return value at runtime, so it isn 't the
               | same functionality.
        
               | lolinder wrote:
               | First of all, can you just reply? It does weird things to
               | the threading when you don't.
               | 
               | Second, removing the default case is part of my point.
               | 
               | You were writing _TypeScript_ code, not raw JS, and in my
               | improved example the type checker won 't let you try to
               | access countryNames["CA"] until "CA" has been added to
               | CountryCode. Once "CA" is added to CountryCode, the type
               | checker won't let you proceed until you've added "CA" to
               | the countryNames. The only situation in which a default
               | case is needed is if you throw an unchecked type
               | assertion into the mix or if you allow implicit any.
               | 
               | With implicit any turned off, this code:
               | const test = countryNames["CA"]
               | 
               | Gives this error:                   TS7053: Element
               | implicitly has an 'any' type because expression of type
               | '"CA"' can't be used to index type 'Record<CountryCode,
               | string>'.   Property 'CA' does not exist on type
               | 'Record<CountryCode, string>'.
        
               | aastronaut wrote:
               | the _reply_ button wasn 't there and I assumed the reach
               | of a depth limit... guess I just needed to wait a bit
               | -\\_(tsu)_/-
               | 
               | Moving the case for a default value to the caller is a
               | weird choice IMHO. Types should reflect the assumed state
               | about a program, but we all know the runtime can behave
               | in unexpected ways. Assume an SPA and the response of an
               | API call was typed with _CountryCode_ in some field, then
               | somebody just worked on the API - I prefer to crash my
               | world close to were my assumption doesn 't fit anymore,
               | but YMMW.
               | 
               | Your implementation (and safety from the type checker)
               | only helps at build time and puts more responsibility and
               | care on the caller. That implementation _could_ prolong
               | the error throwing until _undefined_ reaches the
               | database, mine could already crash at the client. Either
               | TS or JS will do that.
        
               | lolinder wrote:
               | > Assume an SPA and the response of an API call was typed
               | with CountryCode in some field, then somebody just worked
               | on the API - I prefer to crash my world close to were my
               | assumption doesn't fit anymore, but YMMW.
               | 
               | Agreed on crashing, but I prefer to push validation to
               | the boundaries of my process and let the type checker
               | _prove_ that all code _within_ my process is well-typed.
               | 
               | Defensive programming deep in my code for errors that the
               | type checker is _designed_ to prevent feels wasteful to
               | me, both of CPU cycles and programmer thought cycles.
               | Type errors _within_ a process can only occur if you
               | abuse TypeScript escape hatches or blindly trust external
               | data. So don 't cast unless you've checked first, and
               | check your type assumptions about data from untrusted
               | APIs before you assign it to a variable of a given type.
        
         | [deleted]
        
       | grose wrote:
       | I found async generators to be handy while porting a C Prolog
       | interpreter to WASM/JS. A `for await` loop is a natural way to
       | iterate over query results, and as a bonus you can use `finally`
       | within the generator to automatically clean up resources (free
       | memory). There are ways for determined users to leak (manually
       | iterating and forgetting to call `.return()`) but I've found that
       | setting a finalizer on a local variable inside of the generator
       | seems to work :-). I can't say manual memory management is a
       | common task in JS but it did the trick.
       | 
       | The generator in question, which is perhaps the gnarliest JS I've
       | ever written: https://github.com/guregu/trealla-
       | js/blob/887d200b8eecfca8ed...
        
       | nilslindemann wrote:
       | I like generators and using them in for (... of ...) {...} loops.
       | Reminds me of Python joy.
        
       | kaleidawave wrote:
       | It's silly how the 'async' function modifier got a _keyword_
       | before the function keyword and the generator modifier got a
       | symbol '*' after the function keyword...
        
       | Waterluvian wrote:
       | Python is where I developed my (naturally flawed but possibly
       | useful) mental model for generators: you are "pulling" data out
       | of them.
       | 
       | Instead of pushing data into a processing grinder, watching the
       | sausage links pour out the other side, whether you're prepared or
       | not, you're pulling each sausage on-demand, causing the machine
       | to move as a consequence of wanting an output.
       | 
       | I'm sure smarter people appreciate generators more than I do.
       | They're useful for co-routines and other wizardry. But I
       | personally just find the mental model more useful in some cases,
       | especially knowing it keeps memory and CPU usage more in-line
       | with my needs. Doubly especially if my generator may not have an
       | upper bound.
        
         | throwawaymaths wrote:
         | I mean that's fine but the language decision that makes my
         | small brain break is "why call it a function, instead of
         | calling it a completely different name to represent the
         | completely different things that it is"?
        
           | Waterluvian wrote:
           | I never thought of that, and now I'm asking the same
           | question.
           | 
           | Perhaps there's history or mathematical explanations to this.
           | But, yeah, "A function that can temporarily pause itself and
           | be returned to later" is a possibly confusing overloading of
           | the concept of a function.
        
             | throwawaymaths wrote:
             | It makes sense in a low level language like zig, because an
             | async functions there _truly_ is a function frame (in the
             | sense of set up a stack and save your registers to be
             | popped off of it at the end) but in high level languages -
             | and even rust, if my understanding of how async works in
             | rust - it 's different and calling it a function seems
             | incorrect.
        
               | throwawaymaths wrote:
               | Anyone care to explain their downvotes?
        
               | Waterluvian wrote:
               | FWIW I'm really appreciating your comments. These are all
               | models. I really enjoy hearing how others perceive
               | things.
        
             | jayflux wrote:
             | I honestly think the opposite.
             | 
             | When I learned generators from Python the understanding of
             | "a function that yields instead of returns" was very easy
             | to grasp. And considering I already knew how functions
             | worked & what their syntax was this was just an extra step
             | (rather than starting from scratch). ECMAscript have taken
             | a similar path to Python here.
             | 
             | This could also be because my mental model is they are just
             | functions that have the ability to return to the stack at a
             | later point in time, rather than "running to completion".
        
               | throwawaymaths wrote:
               | Well maybe it's my small brain but I remember in math
               | class it was very important that "a function has only one
               | return value", which probably is part of why I was
               | confused.
        
               | WorldMaker wrote:
               | Technically generators still only have one return value:
               | an Iterable. That Iterable represents an object with a
               | "next()" callback and you can entirely write by hand
               | Iterables as simple dumb objects if you like. (The
               | generator function syntax makes it a lot easier to build
               | complex state machines, but most languages aren't going
               | to stop you if you prefer to write your state machine by
               | hand.)
        
               | Dylan16807 wrote:
               | The mathematical definition of function also rejects all
               | side-effects, so probably not the best thing to use
               | around programming.
               | 
               | Call it a routine instead?
        
               | Waterluvian wrote:
               | Yup, I can see that! One issue I have is that my model of
               | the stack is that "it's a literal _stack_ of frames. You
               | pop them one by one, executing them."
               | 
               | What happens to a frame that you pause? Does it get set
               | aside? Is it still on the stack?
        
               | hundt wrote:
               | This article[0] and a clarifying comment[1] answer the
               | question for JS. I think it's the same for Python.
               | Apparently your model (which was mine, too) is out of
               | date!
               | 
               | [0] https://hacks.mozilla.org/2015/05/es6-in-depth-
               | generators/
               | 
               | [1] https://hacks.mozilla.org/2015/05/es6-in-depth-
               | generators/#c...
        
           | activitypea wrote:
           | This was killing me when back when I was a Python dev. A
           | "generator" is just a callable object made less reasonable.
        
             | throwawaymaths wrote:
             | Even callable objects are kinda brain breaking. I used to
             | joke that the duality of callable objects (and
             | getter/setters, function prototypes in js) were as
             | mysterious as the wave/particle duality in quantum
             | mechanics.
        
               | eurasiantiger wrote:
               | Functions are objects too.
        
               | Izkata wrote:
               | I mean, if we're talking python here, functions _are_
               | callable objects.
        
               | throwawaymaths wrote:
               | Good point.
        
       | ravenstine wrote:
       | I initially liked the idea of generators, but after years of
       | trying to find ways to apply them, I just haven't found a use
       | case where they were more sensible than using existing logic and
       | looping constructs. They _could_ be useful where they would
       | provide less overhead than constructing arrays for the same
       | purpose, but that doesn 't mean the same thing can't be achieved
       | without either of those things. It's good in theory, but hasn't
       | been useful to me in practice.
        
         | koliber wrote:
         | Imagine you have function that returns elements. In order to
         | return each element, you need to do some time-consuming
         | calculation. You don't know how many elements the users of your
         | function will need. Some may need 13 items. Some the first 500.
         | Others might be interested in knowing only the first item.
         | 
         | Let's say that your sequence has a maximum size of 1000. If you
         | were to return an array, you'd need to construct the full array
         | each time, even if the code that calls your function only needs
         | 1 item.
         | 
         | Using a generator, you can write the code once, and it is
         | performant across different use cases.
        
           | malcolmstill wrote:
           | I could write this against a few other replies but I will
           | write it here. Moreover, I don't think I'm going to say
           | anything that hasn't already been covered by other people,
           | but I am going to attempt to distil down the arguments.
           | 
           | - The benefits of generators are, in a large part, the
           | benefits of using iterators
           | 
           | - What are the benefits of using iterators? As you say, one
           | benefit is that calling `next` on an iterator performs a
           | single unit of work getting the next value. This let's you
           | avoid e.g. allocating intermediate arrays, let's you do
           | infinite streams, etc. Compare that to calling `map` on a
           | list...you have to consume the entire list.
           | 
           | - A second benefit of iterators is that of scope. When I call
           | `next` on an iterator I get the next value in the scope of
           | the caller. This is particularly useful in a language with
           | function colouring, because use of the `next` value can match
           | what is required of the caller. E.g. the caller may want to
           | await a function using some field of the `next` value and
           | this is totally fine. Compare that to calling `map` with a
           | lambda and wanting to `await` inside the lambda...the problem
           | is you are now in the wrong scope and can't `await`.
           | 
           | - So where do generators come in? Well they are just
           | syntactic sugar that will generate the state machine that you
           | would otherwise have to implement by hand in your iterator.
           | In that sense you don't need generators at all...
           | 
           | - BUT, with generators you can do things that would
           | technically be possible with iterators but would be so clumsy
           | to implement (I'm thinking here of coroutine libraries) that
           | having them as a distinct feature makes sense.
        
         | Izkata wrote:
         | There's two places I've used them in the past year where it was
         | a natural fit:
         | 
         | * Querying solr for a massive amount of data using cursors,
         | where the api and cursor use is hidden so I only have to
         | process the result as a simple loop.
         | 
         | * Pulling csv data from disk and grouping by one of the
         | columns, so the main loop is simply looping over groups without
         | having to do that logic itself.
         | 
         | One of they key points in both versions is that the data can be
         | too big to pre-process, and the system would run out of memory
         | if I tried to load it all in at once.
        
         | heavyset_go wrote:
         | Generators are great for building and consuming lazy iterators,
         | and for building pipelines of lazy iterators. Lazy evaluation
         | in pipelines can interweave work and can be more efficient than
         | immediate evaluation.
        
       | sbf501 wrote:
       | I use them for reading large files that need to be parsed.
       | 
       | A token might require a complex decode, so the generator fires
       | once per token, but keeps the state of the file structure and the
       | parser.
       | 
       | It greatly simplifies the code.
        
       | the_other wrote:
       | Redux-sagas[0] makes great use of generators. I found it a
       | fantastic tool, if you're already in the redux ecosystem, and
       | have an application that sprawls enough to benefit. It's great
       | when you have to manage multiple process lifecycles. A single
       | "saga" can summaries an entire service lifespan in just a few
       | lines of code. Good candidates include a socket connection, a
       | user session, analytics etc. I desperately want to use it with my
       | current project (streaming video) but our code's too mature to
       | introduce such an architectural change.
       | 
       | The downsides are:
       | 
       | - a quirky syntax that needs learning, and is of the "loose with
       | semantics" style - like Rails-eque REST's play with HTTP methods
       | 
       | - it's hard to test (despite what the documentation claims). It's
       | highly declarative, and such code seems hard to test.
       | 
       | [0] http://redux-saga.js.org/
        
         | isakkeyten wrote:
         | Be careful. You will summon acemarke with this comment.
        
           | acemarke wrote:
           | HAH! and ironically I read this literally 10 minutes after
           | you posted it :)
           | 
           |  _dons maintainer hat_
           | 
           | <ObligatoryResponse>
           | 
           | We've been generally recommending against use of sagas for
           | years - they're a power tool, and very few apps need that.
           | They're also a bad fit for basic data fetching.
           | 
           | Today, Redux Toolkit's "RTK Query" API solves the data
           | fetching and caching use case, and the RTK "listener"
           | middleware solves the reactive logic use case with a simpler
           | API, smaller bundle size, and better TS support.
           | 
           | Resources:
           | 
           | - https://redux.js.org/tutorials/essentials/part-7-rtk-
           | query-b...
           | 
           | - https://redux-toolkit.js.org/rtk-query/overview
           | 
           | - https://redux-toolkit.js.org/api/createListenerMiddleware
           | 
           | - https://blog.isquaredsoftware.com/2022/06/presentations-
           | mode...
           | 
           | </ObligatoryResponse>
        
             | mcluck wrote:
             | What's weird is that I still firmly believe that sagas is
             | one of the sanest ways of organizing an application. I
             | built a sort of boilerplate project that shows how I use
             | it[1] but the TL;DR is that I can wrap all of my
             | functionality into nice little sagas and manage the state
             | very easily with lenses. Handling data fetching isn't too
             | complicated either [2] but I'm also not doing any sort of
             | fancy caching in this example.
             | 
             | [1]: https://github.com/MCluck90/foal-ts-
             | monorepo/blob/main/app/c...
             | 
             | [2]: https://github.com/MCluck90/foal-ts-
             | monorepo/blob/main/app/c...
        
             | aastronaut wrote:
             | I'll use your nice response for an actual question/remark,
             | gotcha! =)
             | 
             | Recently I had a look at the kubeshop-dashboard repo[1] and
             | their use of the _RTK Query API_ [2]. When I write the
             | boilerplate for any SPA nowadays, I usually like to merge
             | any fetching logic with the lib-specific
             | notification/toast-methods, in order to render something to
             | the user about any reached warning- or error-timeouts for
             | each ongoing fetch by default. Meaning:
             | 
             | - every new fetch would start a timer
             | 
             | - after 10secs a warning-notification is shown "a fetch
             | takes longer than expected..."
             | 
             | - and after 30secs the _AbortController_ signals the
             | cancelling of the ongoing fetch and an error-notification
             | is shown  "fetch took to long. click to try again."
             | 
             | The implementation of react-query, its "hook-yfied" nature,
             | makes it super easy to wrap it and merge it with the
             | component-lib to create such a thing. I just need to wrap
             | its provided hooks ( _useQuery_ , _useMutation_ ) with
             | "hook-creators" (I usually call them _createQueryHook_ and
             | _createMutationHook_ ) and don't need to dive into any of
             | its implementation specific details. But _createApi_ , as
             | provided by _RTK Query API_ , makes this quite a bit
             | harder, or so it seems to me at least. How would you wrap
             | _createApi_ to provide such a functionality for every fetch
             | by default?
             | 
             | [1]: https://github.com/kubeshop/testkube-dashboard
             | 
             | [2]: https://github.com/kubeshop/testkube-
             | dashboard/tree/main/src...
        
               | acemarke wrote:
               | Hmm. Lenz ( @phryneas in most places) could speak more to
               | some of this, but I don't think RTKQ really supports the
               | idea of "canceling" a fetch in general. Can you give some
               | specifics about what that means in your case?
               | 
               | As for the timers and toasts go, I can think of two
               | possible approaches off the top of my head.
               | 
               | First, you could provide a custom `baseQuery` function
               | [0] that wraps around the built-in `fetchBaseQuery`, does
               | a `Promise.race()` or similar, and then triggers the
               | toasts as needed.
               | 
               | Another could be to use the RTK "listener" middleware [1]
               | to listen for the API's `/pending` actions being
               | dispatched. Each pending action would kick off a listener
               | instance that does similar timer logic, waits for the
               | corresponding `/fulfilled` or `/rejected` action, and
               | shows the toast if necessary .
               | 
               | If you could drop by the `#redux` channel in the
               | Reactiflux Discord [2], or open up a discussion thread in
               | the RTK repo, we could probably try to chat through use
               | cases and offer some more specific suggestions.
               | 
               | [0] https://redux-toolkit.js.org/rtk-
               | query/usage/customizing-que...
               | 
               | [1] https://redux-
               | toolkit.js.org/api/createListenerMiddleware
               | 
               | [2] https://www.reactiflux.com
        
               | aastronaut wrote:
               | > Can you give some specifics about what that means in
               | your case?
               | 
               |  _react-query_ has a default behavior to cancel a fetch
               | when the component unmounts (AFAIK), eg. the user changes
               | to another view and the data of the previous view aren 't
               | needed anymore. I prefer to only have those fetches
               | pending which are actually needed and seem likely to
               | succeed, as otherwise my SPAs would just add unnecessary
               | load on the API gateway. I specifically had such a case
               | when the backend team was in transition to a microservice
               | architecture, hence the timeouts.
               | 
               | But thanks, will join the discord then after I created a
               | repo to play around.
        
         | pgorczak wrote:
         | Async generators similarly open up different ways to program
         | complex / multi step user interactions. Instead of creating
         | "state machine" objects that mutate on every input, you can
         | have async functions aka coroutines that iterate over user
         | inputs, making the flow of an interaction much more explicit.
        
         | noob_07 wrote:
         | The only off putting thing in redux-saga for me was the over
         | use of verbs in the API. The `put, call, take, takeEvery, all`.
         | That makes me somehow a little tense. I don't agree with the
         | testing part though, We used helpers like `runSaga` method to
         | get this done easily, also generator value can also be passed
         | in using `generator.next(//value//)` if I remember correctly.
        
         | qudat wrote:
         | > it's hard to test (despite what the documentation claims).
         | It's highly declarative, and such code seems hard to test.
         | 
         | redux-saga member here. If we concede that mocks are inherently
         | difficult to get right and maintain, often requiring libraries
         | or testing frameworks to do a lot of the heavy lifting, then I
         | would argue testing sagas is a breeze in comparison.
         | 
         | The real magic behind redux-saga and its use of generators is
         | the end-user functions do not have side-effects at all, they
         | merely describe what those side-effects ought to look like. The
         | library activates the side-effects in its runtime, the end-user
         | just yields to json objects.
         | 
         | So in order to test sagas, all you need is to run the generator
         | and make sure it yields the things you expect it to yield.
         | 
         | https://bower.sh/simplify-testing-async-io-javascript
         | 
         | Having said all that, because the react world has heavily
         | shifted towards hooks -- which explicitly make the view layer
         | impure and hard to unit test -- a bunch of tooling has come out
         | to prefer integration testing. In particular, testing user
         | flows and evaluating what the user sees. In this paradigm,
         | testing individual sagas or even react components is not nearly
         | as valuable.
        
           | sktrdie wrote:
           | First of all, sagas are an amazing concept that I still
           | cherish even if I don't use them anymore (hype not there?).
           | 
           | Regarding the hooks comment I'm not sure I follow. Surely
           | components are also idempotent (if you skip useEffect) and
           | should be replayable - that's at least how I think react
           | refresh can manage to keep state intact during file changes
           | in development.
           | 
           | Anyway I still very much value the concepts behind sagas so
           | thanks for great work.
        
       | scotty79 wrote:
       | Because it's an awesome and easy way to create iterables.
       | class FreakyCollection {           [Symbol.iterator]() {
       | return function*() {               for //...               //
       | iterate over your freaky collection however you please yielding
       | one element by one             }();           }
        
         | jitl wrote:
         | This can be condensed slightly, you don't need the inner
         | function literal. In Typescript:                   class
         | MyCollection<T> {          *[Symbol.iterator]():
         | IterableIterator<MyCollectionEntry<T>> {           for (const
         | thingy of this.thingies) {            yield { ... }           }
         | }         }
        
         | scotty79 wrote:
         | ... also custom and paremetrized iterators:
         | class FreakyCollection {            // ...            *even() {
         | // for ... iterate skipping the odd ones and yielding the rest
         | one by one             }            *having(quality) {
         | // for ... iterate and yield just the items that have some
         | quality             }
         | 
         | and usage then is as simple as:                   for(let item
         | of freakyCollection.having(42)) {
        
       | apineda wrote:
       | I use them to iterate over structs in WASM memory that are neatly
       | packaged as an "object", but are just getters into the current
       | offsets.
        
       | rezonant wrote:
       | I used generators to increase the performance of bitstream
       | parsing by several orders of magnitude over using a promise based
       | system
       | 
       | https://github.com/astronautlabs/bitstream#generators
        
       | BigJono wrote:
       | All you did was turn 15 lines of reasonably readable functional
       | code (that, as another comment pointed out, can be done in 8)
       | into 31 lines that were much harder to read.
        
         | ravi-delia wrote:
         | The price of using simple examples is measured in pages of
         | pedantry
        
       | swalsh wrote:
       | Can I ask what kind of Tea goes best with Tim Tams? Are we
       | talking about just simple black Earl Grey here?
        
       | andrewstuart wrote:
       | I rarely use generators but used one the other day.
       | 
       | I had some code that needed to deal out the next in a sequence of
       | URLs, to functions that were triggered by events occurring at
       | random times.
        
       | javajosh wrote:
       | You can _kinda_ define generators with an arrow function, kinda:
       | const foo = (function*() {           yield 1;        })();
       | console.log(foo.next().value);
       | 
       | I've only written one generator in "real life" and ended up
       | replacing it anyway:                 // A generator function that
       | returns a rotating vector        function*
       | circlePath(stepsPerRotation = 60, theta = 0) {          while
       | (true) {            theta += 2 * Math.PI / stepsPerRotation;
       | yield [Math.cos(theta), Math.sin(theta)];          }        }
        
         | inbx0 wrote:
         | The main benefit of arrow function generators would be the
         | inherited parent-scope this-binding they'd get
        
           | javajosh wrote:
           | Yeesh! I personally stay away from "this" as much as I can,
           | in part because of this effect. I can't imagine using the
           | arrow function's odd "this" behavior intentionally! Plus, I
           | like pure functions a lot so I'd rather just pass in any
           | state through a parameter, which is less error-prone too.
        
       | vbezhenar wrote:
       | I used generator to iterate over AWS objects with automatic
       | paging. Code was very compact and easy to read and write. Other
       | approaches would be worse.
       | 
       | I don't think I ever used it again.
        
       | no_wizard wrote:
       | If generators were treated as first class concerns in JS, they'd
       | be so much more useful, but for them to be useful _today_ you
       | have to do alot of work to make it so in my opinion. We need
       | iterator helpers to make them more useful built in to the
       | standard language.
        
       | sieabahlpark wrote:
        
       | jtolmar wrote:
       | In games, you can use a generator function for stuff that's
       | supposed to take multiple frames. Saves you from either having to
       | write a whole state machine system, or extracting all the state
       | for every multi-frame action out into the object.
        
         | Dylan16807 wrote:
         | Yeah, for things split across animation frames it's great to
         | have the ability to trigger .next() exactly when and where you
         | want to, while still writing straight-line stateful code.
         | 
         | Await lets you do the latter, but not the former.
        
       | oefrha wrote:
       | Async generators should be a very natural interface for many
       | event-based APIs, e.g. WebSocket.onmessage. Unfortunately
       | converting a callback-based API to an async generator is highly
       | nontrivial.
        
         | michaelsbradley wrote:
         | Easily done with Ix:
         | 
         | https://github.com/ReactiveX/IxJS/blob/master/docs/asynciter...
        
       | drawfloat wrote:
       | Not sure about the timtam example. Couldn't this:
       | const flow = (...fns) => x0 => fns.reduce(         (x, f) =>
       | f(x),         x0       );            function
       | slamTimTamsUsingFlow(timtams) {         return timtams
       | .slice(0, MAX_TIMTAMS)             .map(flow(
       | biteArbitraryCorner,                 biteOppositeCorner,
       | insertInBeverage,                 drawLiquid,
       | insertIntoMouth));       }
       | 
       | Just be written as:                 const slamTimTams = timtams
       | =>           timtams             .slice(0, MAX_TIMTAMS)
       | .map(timtam => timtam.biteArbitraryCorner()
       | .biteOppositeCorner()                 .insertInBeverage()
       | .drawLiquid()                 .insertIntoMouth());
       | 
       | Feels much more concise and readable than the flow example
        
         | gcommer wrote:
         | It is a weird example. But that transform does not work
         | generally. Consider if you want to stop when insertIntoMouth()
         | returns a particular value. Or if there is some filter() step,
         | so that you don't know that `n` input items will map to `n`
         | output items.
         | 
         | The difference here is push vs pull (also called eager vs
         | lazy). It is often easier to write pull computations that only
         | do the work that is actually needed.
        
         | zwkrt wrote:
         | Yes. Furthermore I am not really even sure what the motivation
         | was in the first place. I have found generators to be most
         | useful in my career in exactly one circumstance--when I want to
         | abstract some complicated batching, filtering, or exit
         | condition logic out of my main processing loop. As a specific
         | example, generators are great for taking an input stream and
         | returning the first 10000 packets matching '^foo.*' in batches
         | of 10.
         | 
         | In the article, it seems like we are just looping over a set of
         | 11 cookies and doing exactly the same thing to all of them. Not
         | sure what I'm missing. The stuff after that regarding infinite
         | sequences is pretty good though!
        
           | Jtsummers wrote:
           | > In the article, it seems like we are just looping over a
           | set of 11 cookies and doing exactly the same thing to all of
           | them. Not sure what I'm missing.
           | 
           | You're missing that we're _not_ just looping over a set of 11
           | cookies and doing exactly the same thing to all of them. We
           | 're streaming over a lazy sequence of cookies and doing the
           | same thing to them _until we stop_ (in the example when they
           | hit 5 cookies eaten instead of eating them all and,
           | presumably, becoming ill).
           | 
           | Generators act like streams, and are useful in any place you
           | might see this pattern:                 value, state =
           | next_value(state)       -- alternatively if we can mutate the
           | state directly --       value = next_value(state)
           | 
           | But they permit you a greater deal of flexibility about the
           | way "state" mutates than a typical state structure might (or
           | at least greater ease of use).
        
         | jzig wrote:
         | Yup. I found this article difficult to read.
        
         | postalrat wrote:
         | IMO even more readable if we ditch the functional looking
         | stuff:                 function slamTimTam(timtam) {
         | timtam.biteArbitraryCorner();
         | timtam.biteOppositeCorner();         timtam.insertInBeverage();
         | timtam.drawLiquid();         timtam.insertIntoMouth();       }
         | function slamTimTams(timtams) {         const slammedTimTams =
         | timtams.slice(0, MAX_TIMTAMS);              for (const timtam
         | of slammedTimTams) {           slamTimTam(timtam);         }
         | return slammedTimTams;       }
        
           | feoren wrote:
           | > functional looking stuff
           | 
           | That doesn't look "functional" at all. The "slamTimTam"
           | function is modifying an object it's been given. Modifying an
           | object that you've taken as an argument should almost never
           | happen anywhere in any codebase, much less a functional one.
           | 
           | Although it's not totally your fault: TFA seems completely
           | confused about what "map" actually does, itself. I can't
           | really tell whether they know what immutability is or not.
           | 
           | In fact one of my biggest pet peeves is "fluent" style code
           | that _modifies_ the object it 's being called on. D3 in
           | JavaScript is a perfect example of this abominable perversion
           | of that "functional" style.
        
             | horsawlarway wrote:
             | > That doesn't look "functional" at all.
             | 
             | Yes... because he ditched it.
             | 
             | And further - there isn't really anything wrong with
             | modifying an object. Functional programs are nice because
             | they remove lots of extraneous state, but there are many
             | situations where you still _have_ to track state somewhere
             | - doing it in a contained object that is changed is fine.
             | 
             | The issue (particularly in JS) is when you allow assignment
             | by reference value, and not by copy - in that case
             | modifying an object can be dangerous because other parts of
             | the code may be holding onto the same reference, and you've
             | accidentally introduced subtle shared state.
             | 
             | Ideally - all users would be aware, and would make sure
             | that assignment only happens with a call to something like
             | copy()/clone() but the language doesn't really have the
             | tooling to enforce this (unlike many other languages ex:
             | c++/Rust/others)
        
         | [deleted]
        
       | jfengel wrote:
       | Generators are a fairly natural way to write recursive descent
       | parsers. The alternatives are either to parse everything and
       | return it in one big structure (which can be awkward for large
       | documents) or to supply Visitors (which works, but often doesn't
       | match your mental model).
       | 
       | It's nice to be able to write "a lexer just yields a stream of
       | tokens" and "expr ::= term op expr" as:                 function*
       | expr() {          x = term();         operator = op();         y
       | = expr();         yield apply(operator, x, y);       }
       | 
       | Backtracking takes a little setup, but overall it's a very
       | elegant way to write code.
       | 
       | Not many people really need to write parsers, but even if you're
       | using a black box from somebody else, it can be fairly elegant to
       | use if it supplies it as an AST generator or result generator.
        
         | shadowofneptune wrote:
         | In my own lexer in JS, I considered using a generator but
         | instead used a class with an index property. In your
         | experience, what advantages does a generator have over that
         | approach?
        
         | mr_toad wrote:
         | Not just for parsers, you can walk any tree structure with a
         | generator. For example turning a binary tree into a sorted
         | iterable.
        
           | jitl wrote:
           | A bunch of Notion's text editing algorithms need to walk up
           | and down our tree of blocks in a particular order. We
           | implement those walks as generators.
        
         | triyambakam wrote:
         | That's really cool. Do you have any further more detailed
         | examples of writing such a parser using generators? It's very
         | coincidental because I've been learning about this topic
         | recently.
        
         | kevincox wrote:
         | +1 There is a lot of code that is easier to write in "push
         | style" where the code runs blocking and "pushes" results
         | (either to a queue, a callback or a result array that is
         | returned at the end) but it is better for the consumer if it is
         | "pull style" where they can process items as they receive them
         | and can do streaming/incremental parsing. Language supported
         | generation functions/courtesies make it very easy to write in a
         | "push style" while being possible to call in the "pull style".
        
       | _greim_ wrote:
       | I've found generators to be a great way to keep separate concerns
       | separate. Now you can have a function whose job is to figure out
       | what the next thing is, then consume it eslewhere as a simple
       | sequence.
       | 
       | In the past I'd have a function accept a `visit(x)` callback, but
       | then I had to invent a protocol for error handling and early
       | cancellation between the host and callback.
       | 
       | The ability to just `yield x` and have done with it is a breath
       | of fresh air.
        
       | gbuk2013 wrote:
       | I'm a bit surprised that database query pagination isn't directly
       | mentioned as one of the use cases. The (async) generator wraps
       | the paginated calls to the DB and yields pages or individual
       | documents / rows. It's about as vanilla-CRUD-API scenario as I
       | can think of.
        
         | jitl wrote:
         | Agreed! We do this in Notion's public API library:
         | for await (const block of
         | iteratePaginatedAPI(notion.blocks.children.list, {
         | block_id: parentBlockId,         })) {           // Do
         | something with block.         }
         | 
         | Implemented here: https://github.com/makenotion/notion-sdk-
         | js/blob/90418939a90...
        
         | inglor wrote:
         | We've made every node stream async iterable, and we also
         | support all the iterator helpers. If whatever you're using is a
         | Node stream - this works and it exposes an async iterable
         | stream :)
        
       | jitl wrote:
       | You can use One Weird Trick with generator functions to make your
       | code "generic" over _synchronicity_. I use this technique to
       | avoid needing to implement both sync and async versions of some
       | functions in my quickjs-emscripten library.
       | 
       | The great part about this technique as a library author is that
       | unlike choosing to use a Promise return type, this technique is
       | invisible in my public API. I can write a function like `export
       | function coolAlgorithm(getData: (request: I) => O | Promise<O>):
       | R | Promise<R>`, and we get automatic performance improvement if
       | the caller's `getData` function happens to return synchronously,
       | without mystery generator stuff showing up in the function
       | signature.
       | 
       | Helper to make a function that can be either sync or async:
       | https://github.com/justjake/quickjs-emscripten/blob/ff211447...
       | 
       | Uses: https://cs.github.com/justjake/quickjs-
       | emscripten?q=yield*+l...
        
         | jupp0r wrote:
         | The problem with this is that it breaks some tooling (async
         | stack traces in Chrome dev tools for example).
        
         | c-baby wrote:
         | This seems a lot more complicated than something like this:
         | if (!(value instanceof Promise)) {             value =
         | Promise.resolve(value)         }
         | 
         | And then you write the rest of your code as if it is async.
        
           | jitl wrote:
           | It's very easy to make sync code async. Just add an `async`
           | to the function declaration. You don't need the `instanceof`
           | check or `Promise.resolve` - you can `await` any type of
           | value; it'll get unwrapped as a Promise if it has a `then`
           | method, or otherwise just give you back the value. See [MDN
           | for await].
           | 
           | If that's okay for your code, then go for it. However,
           | downgrading a sync function to be async has serious
           | performance implications - turning a trivial sync function
           | call in a tight loop into an awaited async function call will
           | make your loop 90%+ slower [benchmark]. You're also going to
           | allocate much more memory for Promise objects. The code I
           | linked above is from a library implementing a sandboxed
           | Javascript VM. If we forced all calls into the guest sandbox,
           | or from the guest sandbox back to host functions like
           | `console.log`, to be async, the VM would be unusable for many
           | applications.
           | 
           | [MDN for await]: https://developer.mozilla.org/en-
           | US/docs/Web/JavaScript/Refe...
           | 
           | [benchmark]: https://jsbench.me/y0la7auape/2
        
             | c-baby wrote:
        
             | [deleted]
        
           | tobr wrote:
           | Not _as if_. You're making a synchronous call async. And so
           | it keeps spreading through your code. Op's trick is certainly
           | more complicated but looks like a smart way to support async
           | cases but still letting synchronous calls stay synchronous.
        
         | athanagor2 wrote:
         | I used a very similar pattern in a web app, in which the user
         | creates objects by clicking in different places. There is a
         | state built by each click, and it is convenient to just go back
         | one or several steps behind sometimes. So you have something
         | like                 const input1 = yield       // arbitrary
         | side effects, UI updates etc.       const input2 = yield
         | // ...
         | 
         | Each time the user makes an input, it is accumulated in a list,
         | and sent back to the generator function. When the user decides
         | to cancel the last step, a generator is recreated and rerun,
         | with each yield sending back the user input that was stored in
         | the list, except the last one. This requires writing the
         | generator function in a particular way (you have to avoid
         | setting "external" state), but it works and is more flexible
         | than automata, I think.
        
       | vanderZwan wrote:
       | Combine them with a decent coroutine library and you can write
       | relatively straightforward singlethreaded concurrency code.
       | Ramsey Nassr has been exploring that:
       | 
       | [0] https://merveilles.town/@nasser/107892762993715381
       | 
       | [1] https://jsbin.com/mupebasiro/edit?html,console,output
        
         | whatwherewhy wrote:
         | What's the reasoning for using this over promise and/or async?
        
           | jbluepolarbear wrote:
           | I use generator coroutines pretty extensively in my game
           | framework. It makes for some clean cooperative code that's
           | fun to write.
           | 
           | https://github.com/jbluepolarbear/Bumble
        
           | vanderZwan wrote:
           | Fair question! Well, look at the part in the code example in
           | the second link where it goes:                   // then
           | either cancel, drag/drop, or click         yield* coro.first(
           | /* ... */         );
           | 
           | In this example that function takes three generator
           | functions. Whichever of the three yields a value first "goes
           | through", and coro.first() then _aborts the other two_. The
           | resulting code reads a lot like how you would describe it:
           | 
           | "first detect a click on the rectangle, then either cancel
           | the repositioning if escape key is pressed, move the
           | rectangle if the mouse moves, or drop it if a click happens"
           | 
           | The structure is a lot more like the "if/else" kind of
           | control flow structures that most people are more familiar
           | with. On top of that it's deterministic (technically, single-
           | threaded use of things like setTimeout also are but because
           | of how you would structure this it is easier to reason
           | about).
           | 
           | Another way to look at it would be to say that this way of
           | expressing things aligns better with solving the problem in
           | terms of state machines (and with UI that often is quit a
           | nice approach).
           | 
           | This is know as the structured _synchronous_ concurrency
           | paradigm and it 's actually quite nice for certain types of
           | (singlethreaded) concurrency, especially complex UI events.
           | Ceu is a language that goes a bit deeper into this, as well
           | as the Blech language (both targeting embedded contexts -
           | button presses changing machine settings are places where FSM
           | are a natural fit).
           | 
           | http://ceu-lang.org/
           | 
           | https://www.blech-lang.org/
        
       | jongjong wrote:
       | I started using async generators years ago and using for-await-of
       | loops to consume streaming data and be guaranteed that the
       | processing order is maintained when there are async operations to
       | be performed on each chunk of data. It's difficult to do in any
       | other way; you'd need some kind of queue structure which would
       | make code very difficult to read.
        
       | qsort wrote:
       | They were introduced when JS went through its "let's copy python"
       | phase.
       | 
       | They're kind of crippled because generators only really become
       | useful if you have the equivalent of itertools to build a sort of
       | iterator algebra. I love generator-based code in python but it's
       | hard to replicate that without having the library too.
        
         | WorldMaker wrote:
         | That's a call out in the article, too. There's a Stage 2
         | proposal before TC-39 to add a bunch of useful library
         | functions out of the box to the language. In the mean time
         | there's an itertools.js "port" and also IxJS which mirrors RxJS
         | (and .NET LINQ).
         | 
         | (I've used IxJS to good effect in JS apps.)
        
       | 8note wrote:
       | I really like using generators in test code for setting
       | mock/stub.
       | 
       | You can be really specific about how you want it to run, without
       | having to memorize a new language of function calls that have to
       | be triggered in specific orders
        
       | rbalicki wrote:
       | For a concrete example, Relay uses them to garbage collect data
       | in an asynchronous fashion. If an update is detected during the
       | course of a gc, it is aborted.
       | 
       | https://github.com/facebook/relay/blob/main/packages/relay-r...
        
       | munificent wrote:
       | My favorite use case for generators (I was using C# at the time,
       | but it applies equally well to any language with yield) was for
       | implementing a turn-based game loop:
       | 
       | http://journal.stuffwithstuff.com/2008/11/17/using-an-iterat...
        
         | mrozbarry wrote:
         | I have done something similar,
         | https://github.com/mrozbarry/mob-turnbased-rpg/blob/master/s...
        
       | whycombinetor wrote:
       | Strictly speaking, there's no reason anyone "needs" generator
       | functions. Instead of pausing the function execution at each
       | `yield` statement, you can just write a regular (non-generator)
       | function that returns a struct including all the local variables
       | used in the function, and repeatedly call the function with the
       | returned struct as the first argument (or `reduce`). Admittedly
       | this is just writing the exact mechanism of yield in a different
       | way just to avoid using `yield`, but that's the point, it's not
       | necessary to have it built in to the language.
        
         | stevewatson301 wrote:
         | For anyone trying to get a better idea of what parent means,
         | the PHP Generators RFC[1] shows the equivalence between
         | generators and iterators. Iterators are what OP means by
         | "struct including all the local variables used in the
         | function".
         | 
         | [1] https://wiki.php.net/rfc/generators
        
       | heloitsme22 wrote:
       | Have you ever seen the rain?
        
       | teucris wrote:
       | > Repeat until there are no more Tim Tams, or you feel physically
       | ill.
       | 
       | Similar to consumption of generator functions.
        
       | Pandavonium wrote:
       | > Arguably, Australia's greatest cultural achievement is the Tim
       | Tam.
       | 
       | After eating half a packet with my cuppa this morning (and
       | feeling somewhat queasy for it), I can confirm timtams are one of
       | our finest accomplishments.
        
       | irrational wrote:
       | My main takeaway from this is that I need to find a place to buy
       | TimTams in the USA.
        
         | bdickason wrote:
         | Same - I think this might be a clever timtam add disguised as a
         | programming article !
        
           | WorldMaker wrote:
           | At least in Kentucky both Kroger and Target have them
           | randomly in the imports section of the cookie aisle if you
           | look for them. (Depending on supply chain presumably.) In
           | Kroger it's often the hard to see/hard to find top shelf.
           | They rarely get the fun or the especially good flavors, but
           | they often have the originals.
           | 
           | I don't think Kentucky has an especially large Australian
           | influence, so I assume that they distribute Tim Tams somewhat
           | broadly nationally, but I don't know.
        
       | Sniffnoy wrote:
       | I've used JS generator functions at work. Part of it is redux-
       | saga, which has already been mentioned, but part of it is another
       | use.
       | 
       | We use generators to implement functions that can make requests
       | to the caller for more information. So the caller f calls the
       | generator function g, creating an iterator, and then calls
       | iter.next(...) in a loop. If the result is not done then the
       | return value is a request from g to f for more information, which
       | f supplies. If the result is done then g has returned a value and
       | f can use it.
       | 
       | The reason we do this is actually an architectural one. In this
       | case, the extra information supplied by f to g comes from an
       | external network, and g lives in an internal library that we
       | don't want making any network requests or having any networking
       | dependencies. My boss came up with this solution, and so far it's
       | worked out pretty well for us!
       | 
       | Note that before we split things up like this, g did make network
       | requests directly, so g was async and its code was full of
       | awaits. After switching it to this new structure, the awaits just
       | became yield*s. :) (And the actual network requests themselves
       | became yields.)
        
       | wefarrell wrote:
       | I do wish that JS had more builtin syntactic sugar around
       | generators (and particularly async generators). They would be a
       | lot more convenient and syntactically cleaner if most of the
       | Array builtins could also be used for iterators. For async
       | iterators the builtins would need to leverage the promise
       | builtins as well, so you could to something like:
       | const result = await
       | asyncIterator.map(doSomethingAsync).reduce(asyncAggregatorFn)
       | 
       | Without ever having to load all of your data into memory.
        
         | michaelsbradley wrote:
         | You might be interested in what IxJS has to offer:
         | 
         | https://github.com/ReactiveX/IxJS
         | 
         | RxJS is for observables. IxJS is for iterables. Or, a bit more
         | accurately, as explained in the readme:                  IxJS
         | unifies both synchronous and asynchronous pull-based
         | collections, just as RxJS unified the world of push-based
         | collections. RxJS is great for event-based workflows where the
         | data can be pushed at the rate of the producer, however, IxJS
         | is great at I/O operations where you as the consumer can pull
         | the data when you are ready.
        
         | mirekrusin wrote:
         | You may be interested in:
         | 
         | - https://github.com/preludejs/async-generator
         | 
         | - https://github.com/preludejs/generator
         | 
         | ...and example usage https://observablehq.com/@mirek/project-
         | euler
        
         | c-baby wrote:
        
       | Frotag wrote:
       | You can also implement context managers / try-with-resources via
       | generators. Not that I've seen it in the wild or recommend it. I
       | remember it being annoying to debug for some reason.
       | 
       | https://stackoverflow.com/questions/62879698/any-tips-on-con...
        
       | talkingtab wrote:
       | Transducers. See Richard Hickey's talk about transducers here:
       | https://www.youtube.com/watch?v=6mTbuzafcII
       | 
       | You can implement transducers in JavaScript using generators. It
       | was somewhat mind bending but fun.
        
       | AtNightWeCode wrote:
       | Block /assets/book-bg.jpg or remove the background from
       | body::before to make it readable. Article still probably about
       | JS.
        
       ___________________________________________________________________
       (page generated 2022-11-07 23:01 UTC)