0

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
geoffrey
  • 2,080
  • 9
  • 13
  • I don't understand what you mean by "*will produce an error*" - are you saying that it returns the `Left`/`Nothing` value that was passed for `a` or `b`, or do you mean that it throws an exception ("*blow up*") and the program doesn't work or something? – Bergi Jul 26 '20 at 15:48
  • "*I suppose could be a problem depending on what you expect.*" - you mean people would expect the `a` value when both are `Left`? That depends on the implementation of `ap`, and would need to be documented there. You still could flip your function to swap the results. – Bergi Jul 26 '20 at 15:52
  • There is no error. `ap` doesn't expect a partially applied function. It expects a partially applied function in a context. With `Maybe` the context is a computation that may not yield a result. Not only `a`/`b` may be `Nothing` but also the result of `map`. –  Jul 26 '20 at 16:25
  • I have updated my question with more details. When I refer to the `value` I mean the value inside the context: `this.value` in the implementation I just added. – geoffrey Jul 26 '20 at 16:28
  • 2
    The problem with your sample code is that `Right.prototype.ap` is broken. It needs to distinguish the `a` argument into `Left` and `Right`, it cannot just access its `.value`. – Bergi Jul 26 '20 at 16:29
  • @Bergi I really mean an exception in the case of `b` being a `Right`and `a` a `Left`. Concerning the case where `a` and `b` are both a `Left`, this concern is not as great as the first one I expressed and I can't point my finger on what would be problematic exactly with this outcome other than loosing the context which produced the first `Left` (a). I feel that generally speaking if the `Left` is an error or something you probably want to check if it is recoverable before you ignore it or you may want to do something with the error to cook an error message or log it somewhere – geoffrey Jul 26 '20 at 16:35
  • @Bergi I understand that this is broken but is there a specification of what `ap` should do in this case? should `Right.ap` return a `Left` if the argument `a` is a `Left`? Should even `Right` know about the `Left` type at all? I have the feeling this is not (at least aesthetically) compatible with this two-headed implementation. – geoffrey Jul 26 '20 at 16:49
  • 1
    @geoffrey Well, it *cannot* return a `Right` of the expected output type since there's no value to construct one, so it [has to](https://stackoverflow.com/q/10205793/1048572) return the `Left` value. In this particular case, it's basically specified by the monad interface. – Bergi Jul 26 '20 at 16:56
  • 1
    @geoffrey "*Should even Right know about the Left type at all?*" - they are not separate, different types, they're not types at all. They're value constructors for one and the same type: `Either`. Yes, any `Either` method needs to know about both `Left` and `Right`. Your implementation with two `class`es is a bit weird in that regard. Of course you can emulate pattern matching with dynamic dispatch, but `ap` needs to do it twice. – Bergi Jul 26 '20 at 16:57
  • I think you pointed exactly what was wrong about my reasoning. I was confusing polymorphism with types. – geoffrey Jul 26 '20 at 17:04
  • @scriptum could you recommend some readings on this topic or keywords to type in a search engine? I seem to be lacking some serious academic foundations – geoffrey Jul 26 '20 at 17:20
  • 1
    @geoffrey http://learnyouahaskell.com/chapters – Bergi Jul 26 '20 at 18:44
  • 1
    @geoffrey Shameless plug: I updated the chapter on [type theory](https://github.com/kongware/scriptum/blob/master/course/ch-012.md) of my course. It's still needs typo fixing etc but it's readable. If you are still interested.. –  Jul 28 '20 at 16:33

1 Answers1

1

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
melston
  • 2,198
  • 22
  • 39