Search code examples
javascripttypescriptasync-awaitgeneratorredux-saga

Can a framework like Redux-Saga achieve the same without use of generators? If not, why not?


I am designing the API of a business-logic library. It is similar in goals and approach to Redux-Saga - managing side-effects and change-propagation of a central store while eliminating any Redux or React dependency.

Can anyone detail specific benefits that means redux-saga has to use generators rather than, for example, offering decorators for async functions (that would intercept callArguments and returnValues and make them available for middleware). I need to decide what strategy to use.

Redux-Saga uses synchronous generator functions declared with * and using yield to define business logic. This is in contrast to code for side-effects composed from asynchronous Promise-based functions declared with async and using await.

In https://github.com/redux-saga/redux-saga/issues/987 Mateusz Burzyński the maintainer of redux-saga states ‘redux-saga cannot be rewritten to async/await’. He goes on ‘Saga being an interpreter of effects is granting us the full control of how and when those effects are resolved’ and ‘we just can't use anything other than generators to reach our goals, because only them are giving us the needed flexibility’.

However, I believe that wrapping async calls could also allow interception of how and when async calls were resolved. I ask myself what did he mean?

So far I have modelled my logic-layer’s ‘sagas’ on the generator-based approach of Redux-Saga and I'm fairly happy with it. However developer feedback so far is basically ‘why are you using yield not await?’ (this is from non redux-saga users who might complain also about redux-saga).

Their view has been that using yield alienates developers who find generators to be unfamiliar magic compared to async/await code, so I find myself having to justify the approach and sometimes I struggle.

Functions based on either await or yield have a lot in common. In each case steps can be run strictly in sequence, they can delegate to other functions, they ‘pause’ while they wait for the next event and they keep implicit state according to where they are ‘up to’ within the sequential procedure, without having to define any additional state to manage this. I am finding it difficult to articulate clearly what CANNOT be expressed through await and CAN ONLY be expressed through yield to justify the initial alignment of my API with redux-saga and generators. Is there anything I can say to justify it?

Under the hood the primitives in my framework already provide async-based implementations for editing and tracking changes in the store (eliminating Redux reducers and redux-saga middleware) and for event queues (fulfilling the need for action-consume patterns like channels and takeLatest). However, the async operations I’ve written have then been wrapped again in ‘Actions’ to present them in the generator layer. This makes me speculate that the generator layer isn’t buying me anything compared to just writing functions using async that directly invoke my async layers. Is there's something fundamental I'm missing?

I am beginning to doubt myself and would like to know others’ views why Redux-Saga uses yield, or what particular power is available through a generator approach as opposed to just writing wrappers around async calls.

So far I can think of two main reasons, which seem weak, given yield might alienate many mainstream developers, and assuming async can deliver the same goodness.

  • Synchronous generator code makes business logic tests easier to write and problem cases easier to log for debugging. You can emulate all kinds of events and side-effects without mocking (see redux-saga-test-plan) since you can intercept every instruction and substitute every value arising! Having said this, mocking is a really mature and familiar technique for javascript test authors. Also if the framework introduced wrappers to intercept the core async functions of your app, this interception could be used for mocking and logging in any case.
  • Forcing developers to write logic through synchronous generators creates an artificial barrier to isolate it from async code. You can’t even use the async keyword in a saga. Perhaps this would make people write better business logic separation? Having said this, if we accepted that ‘wild-west’ async code was a legitimate starting point, we could progressively introduce the power of sagas by just wrapping selected async calls for interception, until we had the level of interception we actually need. E.g. our processes could block on await actionMatching() or await selectorChange() to be notified of the initiation or completion of another call in another 'process' or to notice a state transition. This wouldn't need events served up through a generator to enable
    processes to block on yield takeLatest().

Is there a fundamental constraint that means I need generators to fulfil the capabilities of Redux-Saga?


Solution

  • Redux Saga cannot be re-written to use async/await as per the developers of the library - or maybe more accurately, in my understanding, it would be so onerous for the developers of such a library that it's never going to happen.

    From the library's (redux-saga) point of view async/await is to generators like a younger, dummy brother wink You can do much, much more with generators than with async/await. -- Source comment

    and

    redux-saga cannot be rewritten to async/await - that uses Promises and its way harder to coordinate 'parallel' tasks with those. It would also mean a big change in semantics, how things actually work. In example select effect is synchronous - cant achieve that with Promises.

    Saga being an interpreter of effects is granting us the full control of how and when those effects are resolved + effects are just simple object which are easily comparable and can be monitored which is also way harder with Promises. -- Source comment