javascriptfunctional-programmingfuturefluture

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?

Solution

  • 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)