What can you do ?
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:
- Maybe ( the DB may return something, or
nothing
)
- Either ( If some data validation fails for example )
- Fluture ( to replace the promise. Flutures are different from promises! )
Maybe this, maybe not!
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!
We Either go further, or we don't!
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 ! ).
Putting it all together
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!
Jesus Bananas, this is a lot!
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!