Skip to content

Add equivalent to Promise.then to FAQ #944

@SebastienGllmt

Description

@SebastienGllmt

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:

  1. 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);
}
  1. An approach that chains together well as long as you have a pipe function 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

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions