Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document equivalent to Promise.then #944

Open
SebastienGllmt opened this issue Dec 21, 2024 · 1 comment
Open

Document equivalent to Promise.then #944

SebastienGllmt opened this issue Dec 21, 2024 · 1 comment

Comments

@SebastienGllmt
Copy link

SebastienGllmt commented Dec 21, 2024

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

@SebastienGllmt
Copy link
Author

SebastienGllmt commented Dec 29, 2024

I thought about option (3), but it turns out it's trickier than I thought

The implementation for task.ts could look something like

then: async (...args) => {
  return run(function*() {
    return yield* call(() => getPromise().then(...args));
  });
},

or, if you want to tie the lifetime to the parent, it could equivalently be expanded to

then: async (...args) => {
      const newFrame = frame.createChild(function*() {
        return yield* call(() => getPromise().then(...args));
      });
      newFrame.enter();
      return newFrame.getTask();
    },

however, this gets stuck in an infinite loop which I believe is caused by the .enter(), but I'm not fully sure since getting this to work feels like it requires going pretty deep into the inner workings of the continuation library. Maybe the solution is to do something similar to spawn instead?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant