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
- 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 withmap
) – I might suggest Data.Maybe from folktale. Or since you're clearly not afraid of implementing things on your own, make your own Maybe ^_^
Your
map
implementation is not suitably generic to work on any functor that implements the functor interface. Ie, yours only works withMaybe
, butmap
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 aMaybe
that implements the.map
method (eg Data.Maybe referenced above)
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) // => 6There's really no reason for you to implement
curry
on your own if you're already using Ramda, as it already includes curry
Your
pipe
implementation is mixing concerns of function composition and mapping functors (via use ofmap
). I would recommend reservingpipe
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)
- That
Either
you made is probably not the Either functor/monad you're thinking of. SeeData.Either
(from folktale) for a more complete implementation
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
implementsFunctor
andMonad
, so it can behave as both (and like any other interface it implements) ! The same is true forEither
, 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 functiona -> a
, andf :: b -> c
andg :: 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.
- starting with
Maybe(person)
- read
person.names
property - get the first index of
person.names
– is it an array or something? or the first letter of the name? - read the
.value
property?? We're you expecting a monad here? (look at.chain
compared to.map
in theMaybe
andEither
implementations I linked from folktale) - split the value on
/
- join the values with
''
- 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']
). UsingS.pipe
would necessitate hard-coding one of these inputs in the pipeline.S.insert
cannot be used for updating records, butS.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
Access-Control-Allow-Origin Denied Spotify API
Angularjs Multiple Filter with Custom Filter Function
Why Is Immutability So Important (Or Needed) in JavaScript
Componentdidmount Equivalent on a React Function/Hooks Component
Why Is Proxy to a Map Object in Es2015 Not Working
JavaScript Functions Like "Var Foo = Function Bar() ..."
Parsing Url Hash/Fragment Identifier with JavaScript
App.Settings - the Angular Way
Is It Safe to Assume Strict Comparison in a JavaScript Switch Statement
JavaScript If Statement with Multiple Permissible Conditions
Rendering React Components from Array of Objects
How to Perform Flood Fill with HTML Canvas
Javascript: Clear All Timeouts
How to Remove Spaces from a String Using JavaScript
Get Wrong Value in Data Attribute Jquery
Undefined Values in Array(Len) Initializer