I was using Bluebird for doing asynchronous stuff, but now have to do a lot of empty / null / error checks and I don't want to go down the usual if Else route. Am thinking of using monads, but have not yet grokked it completely.
Also I want it to play nicely with ramda's pipe / compose
as most of my other code is neatly encapsulated in functional pipelines. According to many discussions, monadic Futures (Fluture seems to be recommended) are preferred over Promises and support for pipeP and composeP may be removed in future versions.
Fluture seems like a good option as it supposedly plays well with libraries (like ramda) that adhere to fantasy-land specs.
However I am completely lost as to how to go about implementing stuff integrating Ramda's pipe with Fluture. I need help with some example code.
For eg:
I have a DB call that returns an array of Objects. The array may have values, be empty or be undefined. I have a functional pipeline that transforms the data and returns it to the front end.
Sample Promise code:
fancyDBCall1(constraints)
.then(data => {
if (!data || data.length === 0) {
return []
}
return pipe(
...
transformation functions
...
)(data)
})
.then(res.ok)
.catch(res.serverError)
Can somebody give some pointers on a good way to proceed.
So, there are a few things when can do with your code. But first, let's talk about Monads.
In this code there are 3 types of Monads you can use:
nothing
)Let's decompose your code a little bit. The first thing we want to do is to make sure your fancyDBCall1(constraints)
returns a Maybe
. This means that it maybe returns a result, or nothing.
However, your fancyDBCall1
is an async operation. This means that it must return a Future
. The trick here is instead of making it return a future of a value, like Future <Array>
to make it return a Future < Maybe Array >
.
Whoa, that sounds complicated mister!
Just think of it like instead of having: Future.of('world');
You have: Future.of( Maybe( 'world' ) );
Not so bad right?
This way you avoid doing null checks in your code! The following lines would disappear:
if (!data || data.length === 0) {
return []
}
And your example would look something like:
/*
* Accepts <Maybe Array>.
* Most ramda.js functions are FL compatible, so this function
* would probably remain unchanged.
**/
const tranform = pipe( .... );
// fancyDBCall1 returns `Future <Maybe Array>`
fancyDBCall1(constraints)
.map( transform )
.fork( always(res.serverError), always(res.ok) );
See how nice our code looks? But wait, there is more!
So, if you are paying close attention, you know I am missing something. Sure, we are now handling a null check, but what if transform
blows up? Well, you will say "We send res.serverError".
Ok. That's fair. But what if the transform
function fails because of an invalid username, for example?
You will say your server blew up, but it wasn't exactly true. Your async query was fine, but the data we got wans't. This is something we could anticipate, it's not like a meteor hit our server farm, it's just that some user gave us an invalid e-mail and we need to tell him!
The trick here would be go change our transform
function:
/*
* Accepts <Maybe Array>.
* Returns <Maybe <Either String, Array> >
**/
const tranform = pipe( .... );
Wow, Jesus bananas! What is this dark magic?
Here we say that our transform maybe returns Nothing or maybe it returns an Either. This Either is either a string ( left branch is always the error ) or an array of values ( right branch is always the correct result ! ).
So yeah, it has been quite a hell of a trip, wouldn't you say? To give you some concrete code for you to sink your teeth in, here is what some code with these constructs could possibly look like:
First we have a go with Future <Maybe Array>
:
const { Future } = require("fluture");
const S = require("sanctuary");
const transform = S.map(
S.pipe( [ S.trim, S.toUpper ] )
);
const queryResult = Future.of(
S.Just( [" heello", " world!"] )
);
//const queryResult2 = Future.of( S.Nothing );
const execute =
queryResult
.map( S.map( transform ) )
.fork(
console.error,
res => console.log( S.fromMaybe( [] ) ( res ) )
);
You can play around with queryResult
and queryResult2
. This should give you a good idea of what the Maybe monad can do.
Note that in this case I am using Sanctuary, which is a purist version of Ramda, because of it's Maybe type, but you could use any Maybe type library and be happy with it, the idea of the code would be the same.
Now, let's add Either.
First let's focus on our transformation function, which I have modified a little:
const validateGreet = array =>
array.includes("HELLO") ?
S.Right( array ) :
S.Left( "Invalid Greeting!" );
// Receives an array, and returns Either <String, Array>
const transform = S.pipe( [
S.map( S.pipe( [ S.trim, S.toUpper ] ) ),
validateGreet
] );
So far so good. If the array obeys our conditions, we return the right branch of Either with the array, is not the left branch with an error.
Now, let's add this to our previous example, which will return a Future <Maybe <Either <String, Array>>>
.
const { Future } = require("fluture");
const S = require("sanctuary");
const validateGreet = array =>
array.includes("HELLO") ?
S.Right( array ) :
S.Left( "Invalid Greeting!" );
// Receives an array, and returns Either <String, Array>
const transform = S.pipe( [
S.map( S.pipe( [ S.trim, S.toUpper ] ) ),
validateGreet
] );
//Play with me!
const queryResult = Future.of(
S.Just( [" heello", " world!"] )
);
//Play with me!
//const queryResult = Future.of( S.Nothing );
const execute =
queryResult
.map( S.map( transform ) )
.fork(
err => {
console.error(`The end is near!: ${err}`);
process.exit(1);
},
res => {
// fromMaybe: https://sanctuary.js.org/#fromMaybe
const maybeResult = S.fromMaybe( S.Right([]) ) (res);
//https://sanctuary.js.org/#either
S.either( console.error ) ( console.log ) ( maybeResult )
}
);
So, what this tells us?
If we get an exception ( something not anticipated ) we print The end is near!: ${err}
and we cleanly exit the app.
If our DB returns nothing we print []
.
If the DB does return something and that something is invalid, we print "Invalid Greeting!"
.
If the DB returns something decent, we print it!
Well, yeah. If you are starting with Maybe, Either and Flutures, you have a lot of concepts to learn and it's normal to feel overwhelmed.
I personally don't know any good and active Maybe / Either library for Ramda, ( perhaps you can try the Maybe / Result types from Folktale ? ) and that is why i used Sanctuary, a clone from Ramda that is more pure and integrates nicely with Fluture.
But if you need to start somewhere you can always check the community gitter chat and post questions. Reading the docs also helps a lot.
Hope it helps!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With