2

I want to transform an imperative Promise to a functional Task in a principled fashion:

const record = (type, o) =>
  (o[type.name || type] = type.name || type, o);

const thisify = f => f({});

const taskFromPromise = p =>
  Task((res, rej) =>
    p.then(res)
      .catch(x => rej(`Error: ${x}`)));

const Task = task => record(
  Task,
  thisify(o => {
    o.task = (res, rej) =>
      task(x => {
        o.task = k => k(x);
        return res(x);
      }, rej);
    
    return o;
  }));

const tx = taskFromPromise(Promise.resolve(123)),
  ty = taskFromPromise(Promise.reject("reason").catch(x => x));
//                                              ^^^^^ necessary to avoid uncaught error
  
tx.task(console.log); // 123
ty.task(console.log); // "reason" but should be "Error: reason"

The resolution case works but the rejection doesn't, because Promises are eagerly triggered. If I dropped the catch handler I would have to put the entire computation into a try/catch statement. Is there a more viable alternative?

  • Your `task` method expects two parameters but you're passing only one argument. – Bergi Jun 04 '20 at 20:20
  • I don't see how your `Task` is "less imperative" or "more functional" than a promise. If you can transform back and forth between them, they're equal for most purposes. What's the intended difference? – Bergi Jun 04 '20 at 20:22
  • `Promise` immediately invokes the passed function, whereas `Task` creates a form of function composition. Both are fundamentally different operations and I won't list the consequences, since you are most certainly aware of them. Maybe you think `Task` is imperative, because it causes a local mutation to enable sharing. I am not sure yet if multicast is a harmful or desirable property. The answer probably depends on whether you need cancellation or not. –  Jun 04 '20 at 22:37
  • Yes, your `Task` is just a `Promise` with lazy initial execution, not a function. It is this mutation and the sharing of the results of an asynchronous effect that [breaks the monad properties for promises](https://stackoverflow.com/a/45772042/1048572) and makes them imperative tools. (Also your sharing is broken, since it doesn't happen before the promise is fulfilled, and also doesn't consider rejections). – Bergi Jun 05 '20 at 06:59
  • Running a `Task` is the runtime of Javascript. There is no RT at runtime, because all effects are released. Don't sharing the rejection case is a design decision. _Also your sharing is broken, since it doesn't happen before the promise is fulfilled_ - this is a biased statement. `Task`s are no promises. Sharing is only meant for limited use cases. –  Jun 05 '20 at 07:32
  • I'd suggest you take a look at the `Task` designs in some functional js libraries. Your tasks are muddling in between that and promises, they're neither fish nor fowl. – Bergi Jun 05 '20 at 08:02
  • I am not interested in an implementation that sticks to pure theory. My `x.task` is like `unsafePerfomIO`. It is the hinge between pure code and runtime. We have no `IO` type in JS so we have to create our own runtime. How to make this more reliable is a matter of research for me. So if you say `o.task = k => k(x)` is broken I will keep that in mind. I nonetheless think a certain amount of pragmatism is not only okay but necessary in a non-purely functonal language like JS. –  Jun 05 '20 at 08:29

1 Answers1

0

You don't need .catch(x => x), which turns your rejected promise into an resolved one. You shouldn't get an "uncaught error" since you have a catch inside your taskFromPromise function. Removing the .catch(x => x) does work and lands you in .catch(x => rej(`Error: ${x}`)) instead.

However removing .catch(x => x) does throw "TypeError: rej is not a function". From your code sample it it seems like your .task(...) (in tx.task(...) and ty.task(...)) expects two functions. The first one is called if the promise resolves, the second one if the promise is rejected. Providing both functions leaves me with a working code snippet.

const record = (type, o) =>
  (o[type.name || type] = type.name || type, o);

const thisify = f => f({});

const taskFromPromise = p =>
  Task((res, rej) =>
    p.then(res)
      .catch(x => rej(`Error: ${x}`)));

const Task = task => record(
  Task,
  thisify(o => {
    o.task = (res, rej) => // expects two functions
      task(x => {
        o.task = k => k(x);
        return res(x);
      }, rej);
    
    return o;
  }));

const tx = taskFromPromise(Promise.resolve(123)),
  ty = taskFromPromise(Promise.reject("reason")); // removed .catch(x => x)

tx.task(console.log, console.log); // provide two functions
ty.task(console.log, console.log);
//         ^            ^ 
//      if resolved  if rejected
3limin4t0r
  • 19,353
  • 2
  • 31
  • 52
  • I really no nothing about `Promise`s. I figured a rejected promies throws immeditaley if there is no catch handler directly attached. Sorry for the rookie question. –  Jun 04 '20 at 21:17