Flame_Phoenix
Flame_Phoenix

Reputation: 17604

Fluture bimap and fold, what is the difference and when should I use them?

Background

I am using Fluture to abstract Futures.

Let's say I have a function that makes a GET request. This function can succeed or fail.

Upon making a request, if it succeeds, it prints a message, if it fails, it logs the error and executes a command.

axios.get(endpoint, { timeout: timeoutMs })
    .fold(
        err =>
            logger.errorAsync( err )
            .chain( ( ) => cmd.getAsync("pm2 restart app")),
        response => logger.infoAsync( "Great success!" )
    );

Research

I have been reading the API, and I found that bimap and fold both apply a function to success and error:

bimap: Maps the left function over the rejection value, or the right function over the resolution value, depending on which is present.

fold: Applies the left function to the rejection value, or the right function to the resolution value, depending on which is present, and resolves with the result.

Problem

If you have a keen eye, you will know my example doesn't work. I need to use bimap, but I don't get why.

Questions

  1. When should I use bimap and when should I use fold?
  2. What are the main differences between them?

Upvotes: 1

Views: 839

Answers (2)

Avaq
Avaq

Reputation: 3031

Let's first examine their respective type signatures:

bimap :: (a -> c) -> (b -> d) -> Future a b -> Future c d
fold  :: (a -> c) -> (b -> c) -> Future a b -> Future d c

The difference is quite subtle, but visible. There are two major differences:

  1. The return value of the second argument is different: In bimap, both functions are allowed to return different types. In fold, both functions must return a value of the same type.
  2. The final return value is different: In bimap, you get back a Future where the rejection contains a value of the type returned from the left function, and the resolution contains a value of the type returned from the right function. In fold, the rejection side contains a whole new type variable that is yet to be restricted, and the resolution side contains a value of the type returned by both function.

That's a quite a mouthful, and possibly a bit difficult to parse. I'll try to visualize it in diagrams.

For bimap, it looks like the following. The two branches don't interact:

             rej(x)  res(y)
                 |       |
                 |       |
bimap(f)(g):   f(x)    g(y)
                 |       |
                 V       V

For fold, the rejection branch kind of "stops", and the resoltion branch will continue with the return value from f(x) or the return value from g(y):

             rej(x)  res(y)
                 |       |
                 |       |
fold(f)(g):      ->  f(x)*g(y)
                         |
                         V

You can use bimap whenever you'd like to change the rejection reason and the resolution value at the same time. Doing bimap (f) (g) is like doing compose (mapRej (f)) (map (g)).

You can use fold whenever you want to move your rejection into the resolution branch. In your case, this is what you want. The reason your example doesn't work is because you end up with a Future of a Future, which you have to flatten:

axios.get(endpoint, { timeout: timeoutMs })
    .fold(
        err =>
            logger.errorAsync( err )
            .chain( ( ) => cmd.getAsync("pm2 restart app")),
        response => logger.infoAsync( "Great success!" )
    )
    .chain(inner => inner); //<-- Flatten

Flattening a Monad is very common in Functional Programming, and is typically called join, which can be implemented like:

const join = chain(x => x)

Upvotes: 2

Mat&#237;as Fidemraizer
Mat&#237;as Fidemraizer

Reputation: 64943

One might use bimap to both map rejection and resolution in a single step, and the Future will remain rejected or resolved with a new computation.

In the other hand, foldwill handle both rejection and resolution to always produce a resolution (you're folding both cases into the resolution one). One would use fold to either wrap both results into another type (for example Future Either a b) or to treat any branch as successful.

Thus, bimap differs from fold because the first maps both cases and the second turns either case into a resolution.

Sample: bimap:

const flag = true
const eventualNumber1 = !flag ? Future.reject (1) : Future.of (2)
const eventualNumber2 = Future.bimap (x => x * 2) (x => x + 1) (eventualNumber1)

// it'll output 3. 
Future.fork (console.log) (console.log) (eventualNumber2)

Sample: fold:

const flag = false
const eventualNumber1 = !flag ? Future.reject (1) : Future.of (2)
const eventualNumber2 = Future.fold (x => x * 2) (x => x + 1) (eventualNumber1)

// It'll output 2 even when the Future represents a rejection
Future.value (console.log) (eventualNumber2)

Note how fold gives me the full guarantee that eventualNumber2 is a resolution, so I use Future.value which only handle resolutions!

Upvotes: 1

Related Questions