What are the advantages of Promises over CPS and t

2019-02-17 17:38发布

问题:

ES6 Promises

ES6 Promises are finite state machines and thus require complex implementations. Beyond that the Promise/A+ spec comes with a lot of rough edges:

  • overloaded then (map/chain)
  • recursive flattening / then-able assimilation
  • automatic lifting
  • several subscribers (multicast)
  • eager evaluation

Multicast distribution and eager evaluation are among other things the reasons why ES6 promises cannot be cancelled. Additionally, we can't add our own layers of then-ables with specific features, because they are immediately assimilated by recursive flattening.

I am pretty sure there were a lot of good reasons for any of these design decisions. However, now we have an invariable core language feature rather than specific, competing DSLs for asynchronous control flows in userland. Surely, interop is important, but so is the ability to evolve async control flow features without having to take backward compatibility of the whole language into consideration.

Continuation Passing Style

Continuation passing style abstracts from asynchronous control flows, since it gets rid of the return statement. To regain composability we merely need a functor in the context of continuations:

const compk = (f, g) => x => k => f(x) (x => g(x) (k));


const inck = x => k => setTimeout(k, 0, x + 1);

const log = prefix => x => console.log(prefix, x);


compk(inck, inck) (0) (log("async composition:")); // 2

Of course, we want to compose more than two functions. Instead of manually writing compk3 = (f, g, h) => x => k => f(x) (x => g(x) (y => h(y) (k))) etc., a programmatic solution is desired:

const compkn = (...fs) => k => 
 fs.reduceRight((chain, f) => x => f(x) (chain), k);


const inck = x => (res, rej) => setTimeout(res, 0, x + 1);

const log = prefix => x => console.log(prefix, x);


compkn(inck, inck, inck) (log("async composing n functions:")) (0); // 3

This approach completely lacks exception handling. Let's naively adapt the common callback pattern:

const compk = (f, g) => x => (res, rej) =>
 f(x) (x => g(x) (res), x => rej(x));

const compkn = (...fs) => (res, rej) =>
 fs.reduceRight((chain, f) => x => f(x) (chain, x => rej(x)), res);


const inc = x => x + 1;

const lift = f => x => k => k(f(x));

const inck = x => (res, rej) => setTimeout(res, 0, x + 1);

const decUIntk = x => (res, rej) =>
 setTimeout(x => x < 0 ? rej("out of range " + x) : res(x), 0, x - 1);

const log = prefix => x => console.log(prefix, x);


compk(decUIntk, inck) (0)
 (log("resolved with:"), log("rejected with:")); // rejected

compkn(inck, decUIntk, inck)
 (log("resolved with:"), log("rejected with:")) (0); // resolved

This is just a sketch - a lot of effort would have to be invested to achieve a proper solution. But it is a proof of concept I guess. compk/compkn are extremely simple, because they don't have to fight state.

So what are the advantages of complex ES6 promises over continuation passing style and corresponding DSLs like the continuation functor/monad?

回答1:

The disadvantage of any approach that relies on functional composition is that idiomatic JavaScript code sequences are replaced with lists of named functions. Promises themselves suffer from this.

E.g. I see people do what I call callback lite:

let foo = () => Promise.resolve().then(() => console.log('foo'));
let bar = () => Promise.resolve().then(() => console.log('bar'));

foo().then(bar);

This is one approach, but not the only one, and I personally dislike it, the same way I dislike any attempt at replacing JavaScript with English or action lists.

To me, a benefit of promises is that we can avoid the indirection of traditional callbacks altogether and write code in the order things happen, in a forward direction. Arrow functions help:

Promise.resolve('foo')
  .then(foo => {
    console.log(foo);
    return Promise.resolve('bar');
  })
  .then(bar => {
    console.log(bar);
  });

However, this is arguably still an action list.

So to me, the big advantage of ES6 promises is their compatibility with async/await, which let us write idiomatic JavaScript for asynchronous code much like we would synchronous code, albeit not from top-level scope (requires Chrome or Firefox Beta):

(async () => {
  console.log(await Promise.resolve('foo'));
  console.log(await Promise.resolve('bar'));
})();