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 😀

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.

Leave a Comment