1

I'm trying to understand Applicative and Either's Left. Here is the source:

instance Applicative (Either e) where
    pure          = Right
    Left  e <*> _ = Left e
    Right f <*> r = fmap f r

I'm unable to understand the Left e <*> _ = Left e part. It makes no sense because this:

Left (+3) <*> Right 5

Would return Left (+3), while this:

Right (+1) <*> Left 3

would return Left 3. The problem is inconsistency. Why would the do this? I apologize if my question isn't clean enough. Thanks!

xilpex
  • 3,097
  • 2
  • 14
  • 45
  • 3
    The second example wouldn't return `Left 4`. It would return `Left 3` – Fyodor Soikin Sep 18 '20 at 18:17
  • 1
    @FyodorSoikin, no, the first example returns `Left (+3)`. – A. R. Sep 18 '20 at 18:20
  • 4
    Essentially, the rule is: if either operand is a `Left`, return that (return the first one if both are `Left`), and if both are `Right`, just extract the function and value from them, apply one to the other, and rewrap the result in a `Right`. – Robin Zigmond Sep 18 '20 at 18:25
  • It's not clear what you see the inconsistency as, but if it's the type mismatch between `Left (+3)` and `Left 3`, that's just because the instance is for `Either e` for some fixed `e`, whereas your examples use different `e`s. There need be no relation between the `a` and `b` in `Either a b`, and a more realistic and consistent example would note that both `Left "foo" <*> Right 3` and `Right (+3) <*> Left "foo"` evaluate to `Left "foo"`. Both those take place in the instance for `Either String`. – Robin Zigmond Sep 18 '20 at 18:30
  • 1
    You are being confused since you consider `Right function_taking_Int` and `Left some_Int`. Try instead considering what `Right (+1) <*> Left "hello!"` would do. Yes, that's well typed, and uses the `Either String` applicative. – chi Sep 18 '20 at 18:30
  • 2
    Can you say carefully what you find inconsistent about this behavior? (For example, what would the "consistent" behavior be, in your mind?) – Daniel Wagner Sep 18 '20 at 18:42

3 Answers3

4

TL;DR, It's an intentional design decision.

You should think of Right as the "default" state, and Left as the "fallback" state. I do want to make a small correction to your statements above. Left (+3) <*> Right 5 does not produce (+3) as you say, but rather Left (+3). That's an important distinction. The second correction is that Right (+1) <*> Left 3 procues not Left 4, but Left 3. Again, this is important to understand what's going on.

The reason why the <*> operator cannot be symmetric over Either is because the Left and Right constructors don't take the same type. Let's look at the type of <*> specialized to the Either functor:

(<*>) :: Either a (b -> c) -> Either a b -> Either a c

Notice how only the Right side of the first argument is required to be a function. This is so that you can use (<*>) to chain together arguments like this:

Right (+) <$> Right 3 <*> Right 2
> Right 5

But if the first argument were Left 3:

Right (+) <$> Left 3 <*> Right 2
> (Right (+) <$> Left 3) <*> Right 2
> Left 3 <*> Right 2
> Left 3

It also means that you can use (<*>) in general when Left and Right don't have the same type. If Left (+3) <*> Right 5 should produce Left 8, then what should Left (++ "world") <*> Right 5 produce, given that they can both be coerced to the same type, namely Num a => Either (String -> String) a? It's impossible to come up with a satisfactory answer that treats Left and Right equally when they aren't the same type, and a version of Either that was restricted to carrying one type would have severely hampered utility.

This also allows you to treat Left values as exceptional in some way. If at any stage, you end up with a Left value, Haskell will stop performing calculations and just cascade the Left value all the way up. This also happens to match up well with the way a lot of people think about programming. You could imagine creating alternate sets of computations for Left and Right values, but in many cases, you'd just end up filling the Left computations with id anyways, so this isn't too big a limitation in practice. If you want to execute one of a pair of branching computations, you should use regular branching syntax, such as guards, patterns, or case and if statements and then wrap the values up in Either at the end.

A. R.
  • 2,031
  • 13
  • 27
2

Consider this equivalent definition of the instance:

instance Applicative (Either e) where
    pure = Right
    lhs <*> rhs = case lhs of
                      Right f -> fmap f rhs
                      otherwise -> lhs

If lhs isn't a Right, it must be a Left, and so we return it as-is. We don't actually have to match against the wrapped value at all. If it is a Right, we defer to the Functor instance to find out what gets returned.

instance Functor (Either a) where
    fmap f (Right x) = Right (f x)
    fmap _ l = l

Again, I've given a definition that emphasizes that the content of the Left value doesn't matter. If the second argument isn't a Right, we don't have to explicitly match on it; it must be a Left, and we can just return it as-is.

chepner
  • 497,756
  • 71
  • 530
  • 681
2

If you’re wondering how Right … <*> Left … can still return a Left, it’s because of the fmap call in this definition:

instance Applicative (Either e) where
    pure          = Right
    Left  e <*> _ = Left e
    Right f <*> r = fmap f r

If we expand the definition of fmap for Either, then the definition of <*> looks like this:

Left  e <*> _ = Left e
Right f <*> r = case r of
  Left e -> Left e
  Right x -> Right (f x)

Or, written more symmetrically with all the cases spelled out explicitly:

Left  e1 <*> Left  _e2 = Left e1      -- 1
Left  e  <*> Right _x  = Left e       -- 2
Right _f <*> Left  e   = Left e       -- 3
Right f  <*> Right x   = Right (f x)  -- 4

I’ve marked with an underscore _ the values that are discarded.

Notice that the only case that returns Right is when both inputs are Right. In fact, that’s the only time it’s possible to return Right.

In case (4) we only have a Right (f :: a -> b) and a Right (x :: a); we don’t have an e, so we can’t return a Left, and the only way we have to obtain a b is by applying f to x.

In cases (1), (2), and (3), we must return a Left, because at least one of the inputs is Left, so we are missing the a -> b or the a that we would need to produce a b.

When both inputs are Left in case (1), Either is biased toward the first argument.

There is a type similar to Either called Validation which combines its “failure” cases, instead of choosing one or the other, but it’s more constrained: it’s only an Applicative, while Either is both an Applicative and a Monad.

Jon Purdy
  • 53,300
  • 8
  • 96
  • 166