I've tried to go through this using the types, but I'm still having difficulty understanding how it works.
Given:
> :t (==)
(==) :: Eq a => a -> a -> Bool
> :t (<*>)
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
> :t reverse
reverse :: [a] -> [a]
> :t (==) <*> reverse
(==) <*> reverse :: Eq a => [a] -> Bool
Intuitively I can understand that it combines the equality operator with reverse in such a way that it creates a function that checks if a reversed list is equal to the original, but that's really not much more information than what is already pretty obvious.
Could someone break down in more detail the internals of what is actually happening here?
Chris Taylor's answer is just right, but another way of looking at it, that I find more intuitive, is this: what the Applicative
instance of function types does is this:
So basically, if you have f :: t -> a
and g:: t -> b
, the Applicative
instance lets you map functions h :: a -> b -> c
over the a
and b
results, under the assumption that f
and g
will be fed the same argument.
So think of how you'd write the palindrome test in a non-convoluted way:
palindrome :: Eq a => [a] -> Bool
palindrome xs = xs == reverse xs
xs
appears twice on the right hand side of the definition: once as argument to ==
, and a second time as argument to reverse
. This automatically tells you that there is likely a way to use the (->) t
applicative instance to eliminate the duplication. A different, perhaps more intuitive attack on this would be to first rewrite the function to this:
palindrome xs = id xs == reverse xs
...where id x = x
is the identity function, which just returns its argument (and is a standard library function). Now you can rewrite that using the standard Applicative
idiom (f <$> a0 <*> ... <*> an
) to this:
-- Function that feed the same argument value to both `id` and `reverse`,
-- then tests their results with `==`:
palindrome = (==) <$> id <*> reverse
And now we can ask if there is a way to get rid of id
in that rewrite. Since <$>
is just shorthand for fmap
, we can study the Functor
instance for (->) t
, which is just another way to express function composition:
instance Functor ((->) t) where
-- Mapping `f` over a *function* `g` is just the same as composing `f`
-- on the results of `g`.
fmap f g = f . g
One of the most important properties of function composition is that for any function f
:
f . id = f
So applying that to the version of palindrome
above, we get:
-- Since `f . id = f` for all `f`, then `(==) <$> id` is just `(==)`:
palindrome = (==) <*> reverse