So in x <*> y <$> z
, i.e. fmap (x<*>y) z
, you apply the function x<*>y
over the functor-value z
. The <*>
actually knows nothing about the fmapping – the two operators work on completely separate functors! That the first important thing to realise here.
The next is that, if the result of x<*>y
is a function, then the Applicative
instance of the <*>
is actually the function functor. I wish people would stop using that so much, because it's really one of the more confusing instances and generally not the nicest abstraction to choose.
Concretely, f<*>g
is just a clever way of composing the functions f
and g
while also passing the initial input directly to f
. It works thus:
(<*>) :: (f ~ (x->))
=> f (a -> b) -> f a -> f b
i.e.
(<*>) :: (x ->(a -> b)) -> (x -> a) -> (x -> b)
≡ (x -> a -> b) -> (x -> a) -> x -> b
(f <*> g) x = f x $ g x
In terms of data flow, it's this operation:
────┬─────▶ f ──▶
│ │
└─▶ g ──┘
I would rather express this with arrow combinators:
┌───id──┐
────&&& uncurry f ──▶
└─▶ g ──┘
so f<*>g ≡ id &&& g >>> uncurry f
. Granted, that is nowhere as compact, in fact more verbose than the explicit lambda version \x -> f x $ g x
, which frankly would probably be the best. However, the arrow version is the most general version of the three and arguably expresses best what's going on. The main reason it's so verbose is that currying does not work in out favour here; we might define an operator
(≻>>) :: (x->(a,b)) -> (a->b->c) -> x -> c
g≻>>h = uncurry h . g
and then
x <*> y <$> z
≡ fmap (id &&& y ≻>> x) z
≡ fmap (\ξ -> x ξ $ y ξ) z
For the example, we have
(+) <*> (+1) <$> a
≡ fmap (id &&& (+1) ≻>> (+)) z
≡ fmap (\x -> 1 + x+1) z
≡ fmap (+2) z
I first misread your question. The pattern <$>
<*>
much more common than your <*>
<$>
, the following adresses that... perhaps useful for other people.
f <$> y <*> z
can also be written liftA2 f y z
, and liftA2
is IMO rather easier to understand than the equivalent <*>
.
liftA2 :: (a -> b -> c) -> f a -> f b -> f c
What it does is, it takes a combiner-function on values and produces from it a combiner-function on containers. It's a bit similar to zipWith
except for the list instance, it not only combines each element in the a
list with the corresponding element in the b
list, but combines each element in the a
list once with all elements in the b
list, and concatenates the results.
Prelude> Control.Applicative.liftA2 (+) [0,10..30] [0..3]
[0,1,2,3,10,11,12,13,20,21,22,23,30,31,32,33]