functions as applicative functors (Haskell / LYAH)

2019-01-16 11:43发布

Chapter 11 of Learn You a Haskell introduces the following definition:

instance Applicative ((->) r) where
    pure x = (\_ -> x)
    f <*> g = \x -> f x (g x)

Here, the author engages in some uncharacteristic hand-waving ("The instance implementation for <*> is a bit cryptic, so it's best if we just [show it in action without explaining it]"). I'm hoping someone here might help me figure it out.

According to the applicative class definition, (<*>) :: f (a -> b) -> f a -> f b

In the instance, substituting ((->)r) for f: r->(a->b)->(r->a)->(r->b)

So the first question, is how do I get from that type to f <*> g = \x -> f x (g x)?

But even if I take that last formula for granted, I have trouble making it agree with examples I give to GHCi. For example:

Prelude Control.Applicative> (pure (+5)) <*> (*3) $ 4
17

This expression instead appears consistent with f <*> g = \x -> f (g x) (note that in this version x doesn't appear after f.

I realize this is messy, so thanks for bearing with me.

3条回答
贼婆χ
2楼-- · 2019-01-16 11:56

“In the instance, substituting ((->)r) for f: r->(a->b)->(r->a)->(r->b)

Why, that's not right. It's actually (r->(a->b)) -> (r->a) -> (r->b), and that is the same as (r->a->b) -> (r->a) -> r -> b. I.e., we map an infix and a function which returns the infix' right-hand argument, to a function which takes just the infix' LHS and returns its result. For example,

Prelude Control.Applicative> (:) <*> (\x -> [x]) $ 2
[2,2]
查看更多
做个烂人
3楼-- · 2019-01-16 12:09

Going through your original question, I think there's one subtle but very key point that you might have missed. Using the original example from LYAH:

(+) <$> (+3) <*> (*100) $ 5

This is the same as:

pure (+) <*> (+3) <*> (*100) $ 5

The key here is the pure before (+), which has the effect of boxing (+) as an Applicative. If you look at how pure is defined, you can see that to unbox it, you need to provide an additional argument, which can be anything. Applying <*> to (+) <$> (+3), we get

\x -> (pure (+)) x ((+3) x)

Notice in (pure (+)) x, we are applying x to pure to unbox (+). So we now have

\x -> (+) ((+3) x)

Adding (*100) to get (+) <$> (+3) <*> (*100) and apply <*> again, we get

\x -> (+) ((+3) x) ((*100) x)

So in conclusion, the x after f is NOT the first argument to our binary operator, it is used to UNBOX the operator inside pure.

查看更多
太酷不给撩
4楼-- · 2019-01-16 12:16

First of all, remember how fmap is defined for applicatives:

fmap f x = pure f <*> x

This means that your example is the same as (fmap (+ 5) (* 3)) 4. The fmap function for functions is just composition, so your exact expression is the same as ((+ 5) . (* 3)) 4.

Now, let's think about why the instance is written the way it is. What <*> does is essentially apply a function in the functor to a value in the functor. Specializing to (->) r, this means it applies a function returned by a function from r to a value returned by a function from r. A function that returns a function is just a function of two arguments. So the real question is this: how would you apply a function of two arguments (r and a, returning b) to a value a returned by a function from r?

The first thing to note is that you have to return a value of type (->) r which means the result also has to be a function from r. For reference, here is the <*> function:

f <*> g = \x -> f x (g x)

Since we want to return a function taking a value of type r, x :: r. The function we return has to have a type r -> b. How can we get a value of type b? Well, we have a function f :: r -> a -> b. Since r is going to be the argument of the result function, we get that for free. So now we have a function from a -> b. So, as long as we have some value of type a, we can get a value of type b. But how do we get a value of type a? Well, we have another function g :: r -> a. So we can take our value of type r (the parameter x) and use it to get a value of type a.

So the final idea is simple: we use the parameter to first get a value of type a by plugging it into g. The parameter has type r, g has type r -> a, so we have an a. Then, we plug both the parameter and the new value into f. We need both because f has a type r -> a -> b. Once we plug both an r and an a in, we have a b1. Since the parameter is in a lambda, the result has a type r -> b, which is what we want.

查看更多
登录 后发表回答