From a functional perspective Promise
is a poorly designed data type, because it is lawless, an unprincipled abstraction, rather belonging to the quirky part of Javascript.
In this brief post I will demonstrate another flaw of the Promise
type: It intermingles asynchronous computations that have an in-sequence semantics with those that have an in-parallel one.
Why should both forms be distinguished? Because...
- async computations in-parallel are not monads (in-sequence are)
- both result in different algebraic structures
The former statement is clear, provided you know what a monad is. However, the ladder is a bit harder. Both forms of async computations are just very different and thus their approaches to handle different scenarios vary. Let's compare their monoids to illustrate this statement.
Task
- async in sequence
Task
sequentially performs asynchronous computations. It is a monad but also a monoid:
// Task type
const Task = task => record(
Task,
thisify(o => {
o.task = (res, rej) =>
task(x => {
o.task = k => k(x);
return res(x);
}, rej);
return o;
}));
// Task monoid
const tEmpty = empty =>
() => Task((res, rej) => res(empty()));
const tAppend = append => tx => ty =>
Task((res, rej) =>
tx.task(x =>
ty.task(y =>
res(append(x) (y)), rej), rej));
// Number monoid under addition
const sumAppend = x => y => x + y;
const sumEmpty = () => 0;
// some async functions
const delayTask = f => ms => x =>
Task((res, rej) => setTimeout(comp(res) (f), ms, x));
const tInc = delayTask(x => x + 1) (10); // 10ms delay
const tSqr = delayTask(x => x * x) (100); // 100ms delay
// MAIN
const main = tAppend(sumAppend) (tSqr(5)) (tInc(5));
// ^^^^^^^^^ monoid of the base type
main.task(console.log); // logs 31
Do you see how succinct this Task
implementation is compared to a Promise/A+ compliant one?
The monoid takes a monoid from a base type and lifts it into the context of asynchronous computations in sequence, that is, tAppend
takes a monoid from another type and applies it as soon as both async operations have yielded a result. Don't worry if this is too abstract. We will have an example soon.
Parallel
- async in parallel
Parallel
performa asynchronous computations in parallel. It is only an applicative and monoid but not a monad:
// Parallel type
const Parallel = para => record(
Parallel,
thisify(o => {
o.para = (res, rej) =>
para(x => {
o.para = k => k(x);
return res(x);
}, rej);
return o;
}));
// Parallel monoid
const pEmpty = () => Parallel((res, rej) => null);
const pAppend = tx => ty => {
const guard = (res, rej) => [
x => (
isRes || isRej
? false
: (isRes = true, res(x))),
e =>
isRes || isRej
? false
: (isRej = true, rej(e))];
let isRes = false,
isRej = false;
return Parallel(
(res, rej) => {
tx.para(...guard(res, rej));
ty.para(...guard(res, rej))
})
};
// some async functions
const delayPara = f => ms => x =>
Parallel((res, rej) => setTimeout(comp(res) (f), ms, x));
const pInc = delayPara(x => x + 1) (10); // 10ms delay
const pSqr = delayPara(x => x * x) (100); // 100ms delay
// MAIN
const main = pAppend(pSqr(5)) (pInc(5));
main.para(console.log); // logs 6
Parallel
's monoid instance represents the race monoid, i.e. pAppend
picks the result value of the faster one of two asynchronous computations.
Conclusion
Both monoids are completely different, because Task
and Parallel
are different notions of asynchronous computations. Separating them is laborious at first but leads to more declarative, more predictable and more reliable code. There is a transformation between Task
and Parallel
and vice versa, so you can easily switch between both representations.
Read more on functional programming in Javascript in my online course.
Top comments (0)