-
Notifications
You must be signed in to change notification settings - Fork 35
Description
In JS, it's common in a function that isn't async to chain promises together using .then and .catch
However, it's not clear what the effection equivalent to this is (what happens if you're in a function that isn't a generator? Do you just convert everything to promises using run(myGenerator).then((...) => { ... }) and escape effection temporarily)?
There were two suggestions by Charles on Discord for this:
- A simple approach that does not chain together well
function* then<A,B>(operation, fn: (value: A) => Operation<B>): Operation<B> {
return yield* fn(yield* operation);
}- An approach that chains together well as long as you have a
pipefunction from somewhere (lodash, etc. or write your own)
function then<A,B>(fn: (value: A) => Operation<B>): (operation: Operation<A>) => Operation<B> {
return function*(operation) {
return yield* fn(yield* operation);
}
}
// usage
pipe(
operation,
then(lift((val) => val * 2)),
then(lift((val) => String(val)),
then(lift((val) => val.toUpperCase())),
)Differences with Promise.then: typically you're allowed to chain non-async steps (ex: (async () => 5)().then(a => a+1)). Internally (assuming this is a Promise and not a promise-like), this can be implemented by checking if the return type of fn is instanceof Promise. One can do maybe achieve something similar by checking if fn has a generator symbol, but another approach is to just require using lift instead
- Another option is to leverage
Task
Note that effection already defines a Task interface that is both a Promise and an Operation. In this sense, we already have a path for implementing this pattern via run() which returns a Task. The problem though is that the implementation of then/catch/finally on the result of run() do NOT return a Task, but rather return just a Promise which means you can't chain it together.
If we instead had an implementation of Task that allowed chaining, this would also solve the problem nicely. Note that effect-ts, for example,
Other projects
Note that the effect-ts library has a similar design decision. They decided that their equivalent to tasks can only be piped, and that once you convert to a promise-like API, you cannot go back
import { Effect } from "effect"
const task1 = Effect
.succeed(1) // create a task
.pipe(Effect.delay("200 millis")); // okay to combine with pipes
Effect
.runPromise(task1) // convert to promise-like API
.then(console.log);This is different from effection where there is no explicit runPromise, and rather conversion to a promise-like is done lazily if then is ever called