Search code examples
listhaskellreversepalindrome

You can find out if a list is a palindrome using (==) <*> reverse. How does it work?


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?


Solution

  • 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:

    1. "Feed" the same argument value to two functions with the same argument type;
    2. Combine their results with another function.

    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