Using lift with the Either or the Maybe monad

301 Views Asked by At

When I read about the concept of lift, it's implemented like this (in Javascript)

const liftA2 = f => (a, b) => b.ap(a.map(f));

I realise there is a case in which liftA2 will produce an error: when b is a Right/Just and a is a Left/Nothing, because mapping a Left/Nothing will do nothing to the value when we would need it to become a partially applied function.

When a and b are both a Left it doesn't blow up but of course the value of the returned Left is going to be the value of b which I suppose could be a problem depending on what you expect.

Is it a thing to lift a function to be used with these types? Should I systematically guard against these cases explicitly before using such a function? Is the above implementation correct/complete?


You will find more details about the issue bellow

Let us define the function to lift
const add = a => b => a + b;

In the case of a basic Wrapper implementing of, ap and map, we can follow what is happening

class Wrapper {
    constructor(value) {
        this.value = value;
    }
    static of(value) { return new Wrapper(value); }
    map(f) { return Wrapper.of(f(this.value)); }
    ap(a) { return this.map(a.value); }
}

const a = Wrapper.of(1);
const b = Wrapper.of(2);

// liftA2
const tmp = a.map(add); // Wrapper { λ }
b.ap(tmp); // Wrapper { 3 }

But the thing with Either or Maybe is that they have a Left/Nothing case where map and ap are intended to do nothing special

class Left {
    constructor(value) {
        this.value = value;
    }
    static of(value) { return new Left(value); }
    map(f) { return this; }
    ap(a) { return this; }
}

class Right{
    constructor(value) {
        this.value = value;
    }
    static of(value) { return new Right(value); }
    map(f) { return Right.of(f(this.value)); }
    ap(a) { return this.map(a.value); }
}

const a = Left.of(1);
const b = Right.of(2);

// liftA2 
const tmp = a.map(add); // Left { 1 }
b.ap(tmp); // Error because tmp's value is not a function
1

There are 1 best solutions below

0
melston On

My Javascript is a little basic (I don't use it much). But I think, as has been pointed out in the comments, your implementation of ap is not doing what you want it to.

First, take a look at this answer to a similar question about what lifting is in the first place. It is supposed to take a function of n parameters and put it into the context of a given Functor/Monad where each parameter is contained in that Functor/Monad.

In other words, if f: 'a -> 'b -> 'c (a function that takes two parameters of types 'a and 'b and returns a result of type 'c) then we could use a lift2 where:

lift2:: ('a -> 'b -> 'c) -> (M['a] -> M['b] -> M['c])

which would turn f into a function that takes an M[a] and an M[b] and returns an M[c] (where M in this case is your Either or Maybe).

Since I am more familiar with F# I will use that as an example here. If I was to implement lift2 for Option in F# (equivalent to Maybe) I would do it something like this:

let lift2 (f: 'a -> 'b -> 'c) : ('a option -> 'b option -> 'c option) =
  let f2 ao bo =
    match ao, bo with
    | Some a, Some b -> Some(f a b)
    | _, _ -> None
  f2

You see here that I match on both input types. Both have to be a Some to return a Some value. Otherwise I just return a None. In the case of a Result (equivalent to an Either) I would have to determine which way to bias the match. It could go either way in terms of which Left/Error value to return.

Using the above code in FSI I get the following:

> let f' = lift2 f;;
val f' : (int option -> int option -> int option)

> f' (Some(2)) (Some(3));;
val it : int option = Some 5

> f' (Some(2)) None;;
val it : int option = None

> f' None (Some(3));;
val it : int option = None