How do pipes and monads work together in JavaScript?
hook, line and sinker
I can't stress how critical it is that you don't get snagged on all the new terms it feels like you have to learn – functional programming is about functions – and perhaps the only thing you need to understand about the function is that it allows you to abstract part of your program using a parameter; or multiple parameters if needed (it's not) and supported by your language (it usually is)
Why am I telling you this? Well JavaScript already has a perfectly good API for sequencing asynchronous functions using the built-in, Promise.prototype.then
// never reinvent the wheel
const _pipe = (f, g) => async (...args) => await g( await f(...args))
myPromise .then (f) .then (g) .then (h) ...
But you want to write functional programs, right? This is no problem for the functional programmer. Isolate the behavior you want to abstract (hide), and simply wrap it in a parameterized function – now that you have a function, resume writing your program in a functional style ...
After you do this for a while, you start to notice patterns of abstraction – these patterns will serve as the use cases for all the other things (functors, applicatives, monads, etc) you learn about later – but save those for later – for now, functions ...
Below, we demonstrate left-to-right composition of asynchronous functions via comp
. For the purposes of this program, delay
is included as a Promises creator, and sq
and add1
are sample async functions -
const delay = (ms, x) =>
new Promise (r => setTimeout (r, ms, x))
const sq = async x =>
delay (1000, x * x)
const add1 = async x =>
delay (1000, x + 1)
// just make a function
const comp = (f, g) =>
// abstract away the sickness
x => f (x) .then (g)
// resume functional programming
const main =
comp (sq, add1)
// print promise to console for demo
const demo = p =>
p .then (console.log, console.error)
demo (main (10))
// 2 seconds later...
// 101
invent your own convenience
You can make a variadic compose
that accepts any number of functions – also notice how this allows you to mix sync and async functions in the same composition – a benefit of plugging right into .then
, which automatically promotes non-Promise return values to a Promise -
const delay = (ms, x) =>
new Promise (r => setTimeout (r, ms, x))
const sq = async x =>
delay (1000, x * x)
const add1 = async x =>
delay (1000, x + 1)
// make all sorts of functions
const effect = f => x =>
( f (x), x )
// invent your own convenience
const log =
effect (console.log)
const comp = (f, g) =>
x => f (x) .then (g)
const compose = (...fs) =>
fs .reduce (comp, x => Promise .resolve (x))
// your ritual is complete
const main =
compose (log, add1, log, sq, log, add1, log, sq)
// print promise to console for demo
const demo = p =>
p .then (console.log, console.error)
demo (main (10))
// 10
// 1 second later ...
// 11
// 1 second later ...
// 121
// 1 second later ...
// 122
// 1 second later ...
// 14884
work smarter, not harder
comp
and compose
are easy-to-digest functions that took almost no effort to write. Because we used built-in .then
, all the error-handling stuff gets hooked up for us automatically. You don't have to worry about manually await
'ing or try/catch
or .catch
'ing – yet another benefit of writing our functions this way -
no shame in abstraction
Now, that's not to say that every time you write an abstraction it's for the purposes of hiding something bad, but it can be very useful for a variety of tasks – take for example "hiding" the imperative-style while
-
const fibseq = n => // a counter, n
{ let seq = [] // the sequence we will generate
let a = 0 // the first value in the sequence
let b = 1 // the second value in the sequence
while (n > 0) // when the counter is above zero
{ n = n - 1 // decrement the counter
seq = [ ...seq, a ] // update the sequence
a = a + b // update the first value
b = a - b // update the second value
}
return seq // return the final sequence
}
console .time ('while')
console .log (fibseq (500))
console .timeEnd ('while')
// [ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ... ]
// while: 3ms
But you want to write functional programs, right? This is no problem for the functional programmer. We can make our own looping mechanism but this time it will use functions and expressions instead of statements and side effects – all without sacrificing speed, readability, or stack safety.
Here, loop
continuously applies a function using our recur
value container. When the function returns a non-recur
value, the computation is complete, and the final value is returned. fibseq
is a pure, functional expression complete with unbounded recursion. Both programs compute the result in just about 3 milliseconds. Don't forget to check the answers match :D
const recur = (...values) =>
({ recur, values })
// break the rules sometimes; reinvent a better wheel
const loop = f =>
{ let acc = f ()
while (acc && acc.recur === recur)
acc = f (...acc.values)
return acc
}
const fibseq = x =>
loop // start a loop with vars
( ( n = x // a counter, n, starting at x
, seq = [] // seq, the sequence we will generate
, a = 0 // first value of the sequence
, b = 1 // second value of the sequence
) =>
n === 0 // once our counter reaches zero
? seq // return the sequence
: recur // otherwise recur with updated vars
( n - 1 // the new counter
, [ ...seq, a ] // the new sequence
, b // the new first value
, a + b // the new second value
)
)
console.time ('loop/recur')
console.log (fibseq (500))
console.timeEnd ('loop/recur')
// [ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ... ]
// loop/recur: 3ms
nothing is sacred
And remember, you can do whatever you want. There's nothing magical about then
– someone, somewhere decided to make it. You could be somebody in some place and just make your own then
– here then
is a sort of forward-composition function – just like Promise.prototype.then
, it automatically applies then
to non-then
return values; we add this not because it's a particularly good idea, but to show that we can make that kind of behavior if we wanted to.
const then = x =>
x?.then === then
? x
: Object .assign
( f => then (f (x))
, { then }
)
const sq = x =>
then (x * x)
const add1 = x =>
x + 1
const effect = f => x =>
( f (x), x )
const log =
effect (console.log)
then (10) (log) (sq) (log) (add1) (add1) (add1) (log)
// 10
// 100
// 101
sq (2) (sq) (sq) (sq) (log)
// 65536
what language is that?
It doesn't even look like JavaScript anymore, but who cares? It's your program and you decide what you want it to look like. A good language won't stand in your way and force you to write your program in any particular style; functional or otherwise.
It's actually JavaScript, just uninhibited by misconceptions of what its capable of expressing -
const $ = x => k =>
$ (k (x))
const add = x => y =>
x + y
const mult = x => y =>
x * y
$ (1) // 1
(add (2)) // + 2 = 3
(mult (6)) // * 6 = 18
(console.log) // 18
$ (7) // 7
(add (1)) // + 1 = 8
(mult (8)) // * 8 = 64
(mult (2)) // * 2 = 128
(mult (2)) // * 2 = 256
(console.log) // 256
When you understand $
, you will have understood the mother of all monads. Remember to focus on the mechanics and get an intuition for how it works; worry less about the terms.
ship it
We just used the names comp
and compose
in our local snippets, but when you package your program, you should pick names that make sense given your specific context – see Bergi's comment for a recommendation.
naomik's answer is very interesting, but it doesn't seem like she actually got around to answering your question.
The short answer is that your _pipe
function propagates errors just fine. And stops running functions as soon as one throws an error.
The problem is with your pipeAsync
function, where you had the right idea, but you needlessly have it returning a promise for a function instead of a function.
That's why you can't do this, because it throws an error every time:
const result = await pipeAsync(func1, func2)(a, b);
In order to use pipeAsync
in its current state, you'd need two await
s: one to get the result of pipeAsync
and one to get the result of calling that result:
const result = await (await pipeAsync(func1, func2))(a, b);
The solution
Remove the unnecessary async
and await
from the definition of pipeAsync
. The act of composing a series of functions, even asynchronous functions, is not an asynchronous operation:
module.exports = {
pipeAsync: (...fns) => fns.reduce(_pipe),
Once you've done that, everything works nicely:
const _pipe = (f, g) => async(...args) => await g(await f(...args))
const pipeAsync = (...fns) => fns.reduce(_pipe);
const makeACall = async(a, b) => a + b;
const parseAuthenticatedUser = async(x) => x * 2;
const syncUserWithCore = async(x) => {
throw new Error("NOOOOOO!!!!");
};
const makeToken = async(x) => x - 3;
(async() => {
const x = 9;
const y = 7;
try {
// works up to parseAuthenticatedUser and completes successfully
const token1 = await pipeAsync(
makeACall,
parseAuthenticatedUser
)(x, y);
console.log(token1);
// throws at syncUserWithCore
const token2 = await pipeAsync(
makeACall,
parseAuthenticatedUser,
syncUserWithCore,
makeToken
)(x, y);
console.log(token2);
} catch (e) {
console.error(e);
}
})();
This can also be written without using async
at all:
const _pipe = (f, g) => (...args) => Promise.resolve().then(() => f(...args)).then(g);
const pipeAsync = (...fns) => fns.reduce(_pipe);
const makeACall = (a, b) => Promise.resolve(a + b);
const parseAuthenticatedUser = (x) => Promise.resolve(x * 2);
const syncUserWithCore = (x) => {
throw new Error("NOOOOOO!!!!");
};
const makeToken = (x) => Promise.resolve(x - 3);
const x = 9;
const y = 7;
// works up to parseAuthenticatedUser and completes successfully
pipeAsync(
makeACall,
parseAuthenticatedUser
)(x, y).then(r => console.log(r), e => console.error(e));
// throws at syncUserWithCore
pipeAsync(
makeACall,
parseAuthenticatedUser,
syncUserWithCore,
makeToken
)(x, y).then(r => console.log(r), e => console.error(e))