简体   繁体   中英

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?

Start with the type of (<*>)

> : t (<*>)
(<*>) :: Applicative f => f (a -> b) -> f a -> f b

Now (->) k is an instance of Applicative , with implementation

instance Applicative ((->) k) where
    pure a  = \_ -> a
    f <*> g = \k -> f k (g k)

In particular, the type of (<*>) specialised to (->) k is

(<*>) :: (k -> a -> b) -> (k -> a) -> (k -> b)

So the application (==) <*> reverse is

(==) <*> reverse = \k -> (==) k (reverse k)
                 = \k -> k == reverse k

ie it checks that a list is equal to its reverse.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM