How do pipes and monads work together in JavaScript?
Asked Answered
D

2

14

I have looked at similar questions and answers and have not found an answer that directly addresses my question. I am struggling to understand how to use Maybe or Eitheror Monads in conjunction with piping functions. I want to pipe functions together, but I want the pipe to stop and return an error if one occurs at any step. I am trying to implement Functional Programming concepts in a node.js app, and this is really my first serious exploration of either, so no answer will be so simple as to insult my intelligence on the subject.

I have written a pipe function like this:

const _pipe = (f, g) => async (...args) => await g( await f(...args))

module.exports = {arguments.
    pipeAsync: async (...fns) => {
        return await fns.reduce(_pipe)
    }, 
...

I am calling it like this:

    const token = await utils.pipeAsync(makeACall, parseAuthenticatedUser, syncUserWithCore, managejwt.maketoken)(x, y)  
Deepsix answered 24/10, 2017 at 18:11 Comment(3)
There are no "pipes" nor MonadsDomesday
You're looking for monadic chain, not the ordinary function composition. await only helps with promises.Doucette
Is that stray arguments. on line 3 a typo, or some syntax I've never seen before?Dictatorial
D
29

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.

Domesday answered 24/10, 2017 at 19:14 Comment(14)
You should call it compAsync to distinguish it from the usual function composition. Or even compKleisli if you want to be fancy :-)Doucette
@Bergi, oh I meant to make a mention about exporting it as a different name – thanks for the remark. I'll make a little edit.Domesday
Thanks for your answer. Some things are still unclear. Please don't get caught up in the async/await thing. I put that in there because several of my functions that I need to compose were designed async, and it wouldn't work unless it was asynchronous all the way through. I preferred this syntax because 1) I'm familiar with it from C#, and 2 it's the lastest way in nodejs. If I need to do it with 'then', I will make that change. I believe I am mixing too many things I do not understand. If we took the asynchrony out of it, then how would you interrupt the pipe on errors?Deepsix
async/await is Promise.prototype.then behind the scenes - it's just a different syntax – we could' use await but the point is to show that such heavy machinery is not necessary; it's overkill when we get the *exact behavior we want with a simple f(x).then(g)Domesday
.... (cont.): Doing it another way is absolutely OK, but your reasons for it are not good; 1) syntax from another language (C#) has no affect on what JS is capable of expressing; 2) how others are choosing to write their programs, "the lastest way in nodejs", has no direct impact on your preferences – nor does latest imply best – when CoffeeScript was *the latest way, many people translated perfectly good JavaScript programs bases to a dead-end language.Domesday
You ask, "If we took the asynchrony out of it, then how would you interrupt the pipe on errors?"asynchrony and pipe and interrupt are just words/ideas in your mind; they're all choices you get to make – what do you want your program to look like? Using valid JS syntax and without worrying about "pipes" and "monads", show me what you wish it could look like.Domesday
@naomik Please forgive my weakness in this area. But, in the pipe function I built, the first function called makeACall throws an error. I want it to NOT call the rest of the chain and preferably give me back a custom error message. I have been following this book, Beginning Functional JavaScript: Functional Programming with JavaScript Using EcmaScript 6. The example provided within uses map() functions in Maybe, Either, etc. That does what I want. I think the answer to my initial question is "don't use a pipe or comp function." l just want it succinct and to understand this.Deepsix
@DannyEllisJr. you seem fixated on these particular mechanisms, so allow me to short-cut your understanding as Bergi did – you're looking for the monad interface's bind function – you can make your own bind composition function, const kcomp = (f,g) => m => m.bind(f).bind(g) – then you could make a const kcompose = (...fs) => { ... you try here ... } – the k is for KleisliDomesday
I just stumbled upon your answer and am wondering why then of your last sketch doesn't build up the stack (checked in the console)? After all, it's a recursive function and transforms ordinary functions to CPS versions, so it should use the stack as every CPS in Javascript. You probably recall foldk that ultimately blows the stack for huge iterables and uses a similar recursive pattern, if I'm not mistaken.Bahia
@ftor: sorry, I never meant to imply the last then sketch didn't grow the stack; that was only the topic of the loop/recur sketch.Domesday
@naomik No, it actually does not grow the stack.I applied ten functions to then and the stack never grew beyond three frames. I checked it in my browser's dev console and that really confused me. I'll recheck it with another browser.Bahia
then doesn't lead to a growing stack, because it doesn't construct a nested computation. It doesn't construct anything but carries out the static function call sequence quite mechanically. Silly me.Bahia
I can't help but constantly think about your then as a sort of forward composition. I was wondering if we can supply the value last: comp = n => f => x => n === 1 ? f.reduce((acc, g) => g(acc), x) : comp(n - 1) ([x].concat(f));. Applied like comp(4) (inc) (inc) (inc) (sqr) (2). Partially applied: comp4 = comp(4). Maybe you can come up with a solution that drops n?!?Bahia
Spoiler: const comp = f => Object.assign(g => comp([g].concat(f)), {run: x => f.reduce((acc, h) => h(acc), x)}). yay.Bahia
D
4

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 awaits: 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))
Dictatorial answered 15/1, 2018 at 3:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.