Use Fluture with Ramda
Asked Answered
N

2

6

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.

Nourish answered 3/7, 2017 at 6:27 Comment(0)
B
12

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!

Bowls answered 30/7, 2018 at 10:51 Comment(0)
S
4

Not an expert, but since the experts aren't answering I thought I could Maybe help... ;)

The way I understand it, is that you use a Promise or Future to handle the async part of your data flow, and you use a Maybe or Either to handle weird/multiple/null-data.

E.g.: you can make your data transform function handle null like so:

const lengthDoubled = compose(x => x * 2, length);

const convertDataSafely = pipe(
  Maybe,
  map(lengthDoubled)
  // any other steps
);

Then, in your Future, you can do something like:

Future(/* ... */)
  .map(convertDataSafely)
  .fork(console.error, console.log);

Which will either log a Nothing or a Just(...) containing an integer.


Full code sample: (npm install ramda, fluture and ramda-fantasy)

const Future = require('fluture');
const Maybe = require('ramda-fantasy').Maybe;
const { length, pipe, compose, map } = require("ramda");

// Some random transformation
// [] -> int -> int
const lengthDoubled = compose(x => x * 2, length);

const convertData = pipe(
  Maybe,
  map(lengthDoubled)
)


Future(asyncVal(null))
  .map(convertData)
  .fork(console.error, console.log); // logs Nothing()


Future(asyncVal([]))
  .map(convertData)
  .fork(console.error, console.log); // logs Just(0)


Future(asyncVal([1,2,3]))
  .map(convertData)
  .fork(console.error, console.log); // logs Just(6)

Future(asyncError("Something went wrong"))
  .map(convertData)
  .fork(console.error, console.log); // Error logs "Something went wrong"

// Utils for async data returning
function asyncVal(x) {
  return (rej, res) => {
    setTimeout(() => res(x), 200);
  };
};

function asyncError(msg) {
  return (rej, res) => {
    setTimeout(() => rej(msg), 200)
  };
};
Savour answered 3/7, 2017 at 14:52 Comment(2)
Thanks a lot! Things look much clearer. But I was looking for more like: Pipe( Future, ... transformer fns ..., fork )(constraints) Can your code be converted into the above form? Its just that I have a big preference for pipe / compose over method chainingNourish
You could do something like const fork = (rej, res) => f => f.fork(rej, res); const getConvertHandle = res => pipe( Future, map(convertData), fork(console.error, res) ); and call it like getConvertHandle (console.log) ( /* async get */ )...Savour

© 2022 - 2024 — McMap. All rights reserved.