Haskell `回文 = 反转 >>= (==)`

Haskell `palindrome = reverse >>= (==)`

谁能解释一下 Haskell 中这个回文函数是如何工作的:

palindrome :: Eq a => [a] -> Bool
palindrome = reverse >>= (==)

-- type declarations
reverse :: [a] -> [a]
>>= :: Monad m => m a -> (a -> m b) -> m b
(reverse >>=) :: ([a] -> [a] -> b) -> [a] -> b
(==) :: Eq a => a -> a -> Bool

特别是,Monad 类型类在函数上的定义是如何工作的,它如何以某种方式将 (==) 的输入数量从两个列表减少到一个列表?

我认为这将是 的扩展,我在其中解释了函数如何成为仿函数和应用函数。让我从同一句话开始。

我们可以将函数 ((->) r) 视为上下文,其中包含的值一旦应用就会显示出来。这里我们有 reverse 函数,它包含一个反向列表,但是直到我们将它应用到列表中我们才得到它。让我们记住 bind 的类型签名。

>>= :: Monad m => m a -> (a -> m b) -> m b

这里m areverse函数,m里面的a是反转列表。 (==)(a -> m b)m ba -> Bool。所以 >>= returns 一个 m b 这只是一个函数,它接受一个列表和 returns 一个 Bool。这是一致的,因为我们在函数 monad 中,所以它需要一个 monad(函数)和 returns 另一个。只有当我们用一个列表调用返回的那个时,整个机制才会运行。 m am b 都应用于提供的列表。

手动实现 (->) r 的 monad 实例就像

return x = \_ -> x -- aka const
f >>= g = \r -> g (f r) r

如果仔细研究,我们会注意到 >>= 实际上是 (->) r 类型的 <*> 的翻转版本。但是当我说 flipped 时要小心,它并不意味着 g <*> f == f >>= g.

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

这有点令人兴奋,所以系好安全带:-)

monad 是一种接受一个类型参数的类型构造函数。例如,Maybe 就是这样一个类型构造函数。所以,Maybe 是一个 monad,然后 Maybe Int 是那个 monad 中的一个 monadic 值。类似地,IO 是一个 monad,然后 IO Int 是那个 monad 中的一个 monadic 值。

现在,也可以使用具有 两个 类型参数的类型构造函数来创建 monad。为此,我们只需要修复第一个。例如,看一下 Either:它有两个参数,但一个 monad 必须只有一个。所以我们固定第一个参数。因此 Either Bool 是一个 monad,然后 Either Bool Int 是那个 monad 中的一个 monadic 值。类似地,Either String 是一个 完全不同的 monad,然后 Either String Int 是那个 monad 中的一个 monadic 值。

另一方面,看看函数是如何表示的:

a -> b

但这只是中缀运算符的诡计,类似于5 + 42"foo" <> "bar"。这个中缀表示法可以是 "canonicalized" 这样的:

(->) a b

实际上,"function" 可以看作是一个具有两个类型参数的类型构造函数,就像 Either 一样。对于"function"构造函数来说,这些参数都有特定的含义——一个是"function input",另一个是"function output".

好的,现在我们准备好将函数视为 monad。就像 Either 一样,为了做到这一点,我们必须修复第一个类型参数。因此,例如,(->) Bool 是一个单子,然后 (->) Bool Int(也称为 Bool -> Int)是该单子中的一个单子值。类似地,(->) String 是一个 完全不同的 monad,然后 (->) String Int(也称为 String -> Int)是那个 monad 中的一个 monadic 值。

为了给你一个直觉,一种看待它的方式是这样的 monad 中的 monadic 值意味着 "promise" - 也就是说,“你给我一个 String 然后我给你一个 Int”。然后标准的 monadic 组合让您可以将这些承诺组合在一起,就像您编写 IO 动作一样。

到现在为止?好的,很好。

现在让我们看看如何实现绑定(又名 >>=)。 >>= 的签名如下:

(>>=) :: m a -> (a -> m b) -> m b

现在让我们将其专门用于函数。假设现在 m ~ (->) Bool。然后我们有:

(>>=) :: (->) Bool a -> (a -> (->) Bool b) -> (->) Bool b

或者,如果我们以中缀形式重写 (->) 构造函数,我们得到:

(>>=) :: (Bool -> a) -> (a -> (Bool -> b)) -> (Bool -> b)

所以你看 - 绑定采用 "promise of a",然后是从 a 到 "promise of b" 的函数,然后是 returns 和 "promise of b"。那么,实现是微不足道的:

(>>=) :: (Bool -> a) -> (a -> (Bool -> b)) -> (Bool -> b)
(>>=) promiseOfA f = \theBool -> f (promiseOfA theBool) theBool

这里我们创建了一个新的 "promise",它是一个接受 Bool 的函数,这个 "promise" 所做的是将给定的 Bool 传递给 "promise of a",然后将结果a传递给函数f,其中returns一个"promise of b",我们再将相同的Bool传递给函数,最终得到结果 b.

这里注意一下:看看theBool是如何被使用两次的——先传给promiseOfA然后再传给f的结果?这就是 "reducing of the number of arguments" 发生的地方。这是我们两次传递参数的地方。

当然,这不一定只适用于 Bools。任何输入类型都是公平的游戏。所以我们可以这样概括它:

(>>=) :: (input -> a) -> (a -> (input -> b)) -> (input -> b)
(>>=) promiseOfA f = \i -> f (promiseOfA i) i

(与the actual definition from the standard library比较)。

呸!

好的,现在我们终于可以查看您的原始示例了。首先,看reverse。由于它是一个函数,我们可以将其视为 monad (->) [a] 中的 monadic 值 - 即 "promise of [a]",其中 "input" 也是 [a].

然后,(==)的签名:

(==) :: Eq x => x -> x -> Bool

(请注意,我故意将 a 替换为 x:不要将其与 reverse 签名中的 a 混淆 - 它们是两个不同的 as,不必是同一类型)

我们可以将此签名视为一个接受 x 和 returns 另一个 x -> Bool 类型函数的函数。所以:

(==) :: x -> (x -> Bool)

这可以看作是 bind 的第二个参数,a -> m b 类型的参数。为了这样看,我们需要说 a ~ xm ~ (->) xb ~ Bool。所以这里讨论的 monad 是 (->) x - 即 "promise" 输入类型为 x.

但是等等!为了将此函数绑定到 reverse,它们需要在同一个 monad 中!这意味着 x ~ [a]。这反过来意味着 (==) 的类型被固定为:

(==) :: [a] -> ([a] -> Bool)

因此,当我们调用 (>>=) 将其作为第一个参数传递 reverse 并将 (==) 作为第二个参数传递时,我们得到:

reverse >>= (==) 
= \i -> (==) (reverse i) i   -- by my definition above
= \i -> reverse i == i