How Do Pipes and Monads Work Together in JavaScript

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

Function Composition With Monads...not working

first things first

  1. that Maybe implementation (link) is pretty much junk - you might want to consider picking an implementation that doesn't require you to implement the Functor interface (like you did with map) – I might suggest Data.Maybe from folktale. Or since you're clearly not afraid of implementing things on your own, make your own Maybe ^_^


  1. Your map implementation is not suitably generic to work on any functor that implements the functor interface. Ie, yours only works with Maybe, but map should be generic enough to work with any mappable, if there is such a word.

    No worries tho, Ramda includes map in the box – just use that along with a Maybe that implements the .map method (eg Data.Maybe referenced above)



  1. Your curry implementation doesn't curry functions quite right. It only works for functions with an arity of 2 – curry should work for any function length.

    // given, f
    const f = (a,b,c) => a + b + c

    // what yours does
    curry (f) (1) (2) (3) // => Error: curry(...)(...)(...) is not a function

    // because
    curry (f) (1) (2) // => NaN

    // what it should do
    curry (f) (1) (2) (3) // => 6

    There's really no reason for you to implement curry on your own if you're already using Ramda, as it already includes curry



  1. Your pipe implementation is mixing concerns of function composition and mapping functors (via use of map). I would recommend reserving pipe specifically for function composition.

    Again, not sure why you're using Ramda then reinventing a lot of it. Ramda already includes pipe

    Another thing I noticed

    // you're doing
    R.pipe (a,b,c) (Maybe(x))

    // but that's the same as
    R.pipe (Maybe,a,b,c) (x)


  1. That Either you made is probably not the Either functor/monad you're thinking of. See Data.Either (from folktale) for a more complete implementation


  1. Not a single monad was observed – your question is about function composition with monads but you're only using functor interfaces in your code. Some of the confusion here might be coming from the fact that Maybe implements Functor and Monad, so it can behave as both (and like any other interface it implements) ! The same is true for Either, in this case.

    You might want to see Kleisli category for monadic function composition, though it's probably not relevant to you for this particular problem.


functional interfaces are governed by laws

Your question is born out of a lack of exposure/understanding of the functor laws – What these mean is if your data type adheres to these laws, only then can it can be said that your type is a functor. Under all other circumstances, you might be dealing with something like a functor, but not actually a functor.

functor laws

where map :: Functor f => (a -> b) -> f a -> f b, id is the identity function a -> a, and f :: b -> c and g :: a -> b

// identity
map(id) == id

// composition
compose(map(f), map(g)) == map(compose(f, g))

What this says to us is that we can either compose multiple calls to map with each function individually, or we can compose all the functions first, and then map once. – Note on the left-hand side of the composition law how we call .map twice to apply two functions, but on the right-hand side .map was only called once. The result of each expression is identical.

monad laws

While we're at it, we can cover the monad laws too – again, if your data type obeys these laws, only then can it be called a monad.

where mreturn :: Monad m => a -> m a, mbind :: Monad m => m a -> (a -> m b) -> m b

// left identity
mbind(mreturn(x), f) == f(x)

// right identity
mbind(m, mreturn) == m

// associativity
mbind(mbind(m, f), g) == mbind(m, x => mbind(f(x), g))

It's maybe even a little easier to see the laws using Kleisli composition function, composek – now it's obvious that Monads truly obey the associativity law

monad laws defined using Kleisli composition

where composek :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)

// kleisli left identity
composek(mreturn, f) == f

// kleisli right identity
composek(f, mreturn) == f

// kleisli associativity
composek(composek(f, g), h) == composek(f, composek(g, h))

finding a solution

So what does all of this mean for you? In short, you're doing more work than you have to – especially implementing a lot of the things that already comes with your chosen library, Ramda. Now, there's nothing wrong with that (in fact, I'm a huge proponent of this if you audit many of my
other answers on the site), but it can be the source of confusion if you get some of the implementations wrong.

Since you seem mostly hung up on the map aspect, I will help you see a simple transformation. This takes advantage of the Functor composition law illustrated above:

Note, this uses R.pipe which composes left-to-right instead of right-to-left like R.compose. While I prefer right-to-left composition, the choice to use pipe vs compose is up to you – it's just a notation difference; either way, the laws are fulfilled.

// this
R.pipe(map(f), map(g), map(h), map(i)) (Maybe(x))

// is the same as
Maybe(x).map(R.pipe(f,g,h,i))

I'd like to help more, but I'm not 100% sure what your function is actually trying to do.

  1. starting with Maybe(person)
  2. read person.names property
  3. get the first index of person.names – is it an array or something? or the first letter of the name?
  4. read the .value property?? We're you expecting a monad here? (look at .chain compared to .map in the Maybe and Either implementations I linked from folktale)
  5. split the value on /
  6. join the values with ''
  7. if we have a value, return it, otherwise return some alternative

That's my best guess at what's going on, but I can't picture your data here or make sense of the computation you're trying to do. If you provide more concrete data examples and expected output, I might be able to help you develop a more concrete answer.


remarks

I too was in your boat a couple of years ago; just getting into functional programming, I mean. I wondered how all the little pieces could fit together and actually produce a human-readable program.

The majority of benefits that functional programming provides can only be observed when functional techniques are applied to an entire system. At first, it will feel like you had to introduce tons of dependencies just to rewrite one function in a "functional way". But once you have those dependencies in play in more places in your program, you can start slashing complexity left and right. It's really cool to see, but it takes a while to get your program (and your head) there.

In hindsight, this might not be a great answer, but I hope this helped you in some capacity. It's a very interesting topic to me and I'm happy to assist in answering any other questions you have ^_^

How do I collapse Maybe monads in sanctuary js

We can use S.map to transform inner values and S.join to remove unwanted nesting:

const S = require ('sanctuary');
const $ = require ('sanctuary-def');

// getTag :: String -> Object -> Maybe String
const getTag = tag => S.pipe ([
S.get (S.is ($.String)) ('Tags'), // :: Maybe String
S.map (S.splitOn (',')), // :: Maybe (Array String)
S.map (S.map (S.stripPrefix (tag + '='))), // :: Maybe (Array (Maybe String))
S.map (S.head), // :: Maybe (Maybe (Maybe String))
S.join, // :: Maybe (Maybe String)
S.join, // :: Maybe String
]);

getTag ('a') ({Tags: 'a=y,b=z'}); // => Just ('y')
getTag ('z') ({Tags: 'a=y,b=z'}); // => Nothing
getTag ('z') ({Tags: null}); // => Nothing

S.map followed by S.join is always equivalent to S.chain:

//    getTag :: String -> Object -> Maybe String
const getTag = tag => S.pipe ([
S.get (S.is ($.String)) ('Tags'), // :: Maybe String
S.map (S.splitOn (',')), // :: Maybe (Array String)
S.map (S.map (S.stripPrefix (tag + '='))), // :: Maybe (Array (Maybe String))
S.chain (S.head), // :: Maybe (Maybe String)
S.join, // :: Maybe String
]);

This approach does a bit of unnecessary work by not short-circuiting, but S.stripPrefix allows us, in a single step, to check whether the tag exists and extract its value if it is. :)

Updated version which uses S.justs to select the first match:

//    getTag :: String -> Object -> Maybe String
const getTag = tag => S.pipe ([
S.get (S.is ($.String)) ('Tags'), // :: Maybe String
S.map (S.splitOn (',')), // :: Maybe (Array String)
S.map (S.map (S.stripPrefix (tag + '='))), // :: Maybe (Array (Maybe String))
S.map (S.justs), // :: Maybe (Array String)
S.chain (S.head), // :: Maybe String
]);

Ramda: Is there a way to 'fork' a parameter to two functions during pipe?

You could probably extend your async pipeline, using something like tap:

const loginFlow = asyncPipe(
// ... some functions
getHouseList,
tap(compose(dispatch, addHouses)),
tap(unless(list => list.length > 1, list => dispatch(pickHouse(list[0])))),
list => navigate(list.length > 1 ? 'ListScreen' : 'DetailScreen', list)
);

Whether this is worth doing will depend upon your application. If the pipeline is already a longish one, then it would probably be cleaner to add things to the end this way, even if they're not particularly functional sections. But for a short pipeline, this might not make much sense.

You also might want to look at the now-deprecated, pipeP or its replacement, pipeWith(then).

But you asked in the title about forking a parameter. Ramda's converge does exactly that:

converge(f, [g, h])(x) //=> f(g(x), h(x))

This allows you to pass more than two functions as well, and to pass more than one parameter to the resulting function:

converge(f, [g, h, i])(x, y) //=> f(g(x, y), h(x, y), i(x, y)) 

How do I coordinate interactions with multiple impure and asynchronous services in a functional way

Generally speaking, I would advise to use TaskEither as your monad for network calls (it's basically a lazily-evaluated promise that returns an Either (and thus handles errors and is a Promise that always resolves, never rejects), and for file IO I would use IOEither. fp-ts advises Task and IO as monads for async and sync operations.

import * as TE from 'fp-ts/TaskEither'
declare const dbWriteFilename: (user: User, filename: string, t?: Transaction) => TaskEither<SomeDbError, void>
declare const dbFetchFileCount: (user: User, t?: Transaction) => TaskEither<SomeDbError, number>
declare const runInTransaction: <A>(thunk: (t: Transaction) => TaskEither<SomeDbError, A>) => TaskEither<SomeDbError, A>
declare const writeFile: (filename: string, contents: string) => IOEither<SomeFileError, void>

const USER_FILE_MAX = 10;

const saveFile = (user: User, filename: string) => runInTransaction(t => pipe(
dbFetchFileCount(user, t),
TE.chain(TE.fromPredicate(count => count < USER_FILE_MAX, () => 'too many files')),
TE.chain(() => dbWriteFilename(user, filename, t)),
TE.chainIOEither(() => writeFile(filename, contents))
))()

At any point in this "pipe" if a TE fails it will skip any chain function and only execute things in getOrElse or mapLeft type functions that affect the Left type of the TaskEither.

By convention, the Left type of TaskEither represents errors, and Right type represents success values.

Now you don't have to return T/f anymore, you can do something like:

if(Either.isRight(await safeFile(...)()) {
// success!
}

and a fully fleshed example:

pipe(
saveFile(...),
TE.fold(
e => console.error('Whoops!', e),
() => console.log('Great success!')
)
)()

fold takes a TaskEither and returns a Task (in this case it would take a TaskEither<SomeError, void> and return Task (since both functions passed into fold return void).

A more idiomatic way to write sanctuary pipe

Here is my solution:

const S = require ('sanctuary');

// source :: StrMap (Array { r :: Integer })
const source = {
foo: [{r: 1}, {r: 2}, {r: 3}],
bar: [{r: 4}, {r: 5}, {r: 6}],
quux: [{r: 7}, {r: 8}],
};

// result :: Array { r :: Integer, Section :: String }
const result =
S.filter (S.compose (S.flip (S.elem) (['foo', 'bar', 'baz']))
(S.prop ('Section')))
(S.chain (S.pair (s => S.map (S.unchecked.insert ('Section') (s))))
(S.pairs (source)));

There are several things to note:

  • S.pipe does not feel natural to me in this case as there are two inputs (source and ['foo', 'bar', 'baz']). Using S.pipe would necessitate hard-coding one of these inputs in the pipeline.

  • S.insert cannot be used for updating records, but S.unchecked.insert can be used for this purpose. I appreciate being able to suppress type checking when an expression I know to be sound is considered unsound by the type checker.

  • It's not clear to me from your question whether output order is important. Your solution respects the order of the array of section names; mine does not. Let me know whether output order is important.



Related Topics



Leave a reply



Submit