DEV Community

Iven Marquardt
Iven Marquardt

Posted on

FP serves the most delicious abstractions

For instance, if we try to combine two composed applicatives of type Task<Option<number[]>, E> - an async computation that may fail or yields any number of numbers - it gets pretty soon pretty ugly:

// tAp/tMap     = Task functor/applicative
// optAp/optMap = Option functor/applicative
// arrAp/arrMap = Array functor/applicative
// tttx         = Task(Some([1,2,3]));
// ttty         = Task(Some([10,20,30]));

tAp(
  tMap(x_ => y_ =>
    optAp(
      optMap(x => y =>
        arrAp(
          arrMap(add) (x)) (y)) (x_)) (y_))
            (tttx))
              (ttty); // Task(Some([11,21,31,12,22,32,13,23,33]))

We can get rid of the anonymous functions by using point-free style, but the computation still remains hideous and confusing:

const comp = f => g => x => f(g(x));

tAp(
  tMap(
    comp(optAp)
      (optMap(
        comp(arrAp) (arrMap(add)))))
          (tttx))
            (ttty); // Task(Some([11,21,31,12,22,32,13,23,33]))

The problem seems to be the common applicative pattern ap(map(f) (x)) (y). Let's abstract it:

const liftA2 = ({map, ap}) => f => tx => ty =>
  ap(map(f) (tx)) (ty);

const tLiftA2 = liftA2({map: tMap, ap: tAp});
const optLiftA2 = liftA2({map: optMap, ap: optAp});
const arrLiftA2 = liftA2({map: arrMap, ap: arrAp});

comp3(
  tLiftA2)
    (optLiftA2)
      (arrLiftA2)
        (add)
          (tttx)
            (ttty); // Task(Some([11,21,31,12,22,32,13,23,33]))

This is much better. comp3 takes three functions and the resulting composed function takes add and two composed values tttx/ttty and applies add to the inner values. Since applicative computation of the Array type means to calculate the cartesian product this is what we get. Nice.

See an running example and how everything falls into place.

If you want to learn more about FP join my course on Github.

Discussion (0)