基本上,(>>=) 允许您对操作进行排序,以便后面的操作可以根据早期的结果选择不同的行为。 Functor 类型类中提供了您所要求的更纯粹的函数,并且可以使用 (>>=) 派生,但如果您只使用它,您将根本无法对操作进行排序。还有一个名为 Applicative 的中间体,它允许您对操作进行排序,但不能根据中间结果更改它们。
作为一个例子,让我们构建一个从 Functor 到 Applicative 到 Monad 的简单 IO 操作类型。
我们将重点关注GetC 类型,如下所示
GetC a = Pure a | GetC (Char -> GetC a)
第一个构造函数会及时生效,但第二个构造函数应该立即生效——GetC 包含一个可以响应传入字符的函数。我们可以将GetC 转换为IO 操作以提供这些字符
io :: GetC a -> IO a
io (Pure a) = return a
io (GetC go) = getChar >>= (\char -> io (go char))
这清楚地说明了Pure 的来源——它处理我们类型中的纯值。最后,我们将GetC 抽象化:我们将禁止直接使用Pure 或GetC,只允许我们的用户访问我们定义的函数。我现在就写最重要的一篇
getc :: GetC Char
getc = GetC Pure
得到一个字符然后立即考虑的函数是一个纯值。虽然我称它为最重要的功能,但很明显现在GetC 毫无用处。我们所能做的就是运行getc,然后运行io...以获得完全等同于getChar的效果!
io getc === getChar :: IO Char
但我们将从这里开始。
如开头所述,Functor 类型类提供了一个与您正在寻找的函数完全相同的函数,称为 fmap。
class Functor f where
fmap :: (a -> b) -> f a -> f b
事实证明,我们可以将GetC 实例化为Functor,所以让我们这样做吧。
instance Functor GetC where
fmap f (Pure a) = Pure (f a)
fmap f (GetC go) = GetC (\char -> fmap f (go char))
如果你眯着眼睛,你会注意到fmap 只影响Pure 构造函数。在GetC 构造函数中,它只是被“下推”并推迟到以后。这暗示了fmap的弱点,但让我们试试吧。
io getc :: IO Char
io (fmap ord getc) :: IO Int
io (fmap (\c -> ord + 1) getc) :: IO Int
我们已经能够修改我们的IO 类型解释的返回类型,但仅此而已!特别是,我们仍然仅限于获取单个字符,然后运行回IO 来用它做任何有趣的事情。
这是Functor的弱点。因为,正如您所指出的,它只处理纯函数,它会在“计算结束时”卡住,只修改 Pure 构造函数。
下一步是Applicative,它像这样扩展Functor
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
换句话说,它扩展了将纯值注入我们的上下文的概念并且允许纯函数应用程序跨越数据类型。不出所料,GetC 也实例化了Applicative
instance Applicative GetC where
pure = Pure
Pure f <*> Pure x = Pure (f x)
GetC gof <*> getcx = GetC (\char -> gof <*> getcx)
Pure f <*> GetC gox = GetC (\char -> fmap f (gox char))
Applicative 允许我们对操作进行排序,这在定义中可能已经很清楚了。事实上,我们可以看到(<*>) 将字符应用程序向前推动,以便(<*>) 两侧的GetC 动作按顺序执行。我们像这样使用Applicative
fmap (,) getc <*> getc :: GetC (Char, Char)
它允许我们构建非常有趣的函数,比 Functor 复杂得多。例如,我们已经可以形成一个循环并获得无限的字符流。
getAll :: GetC [Char]
getAll = fmap (:) getc <*> getAll
这证明了Applicative 能够一个接一个地对动作进行排序。
问题是我们不能停下来。 io getAll 是一个无限循环,因为它只会永远消耗字符。例如,当它看到'\n' 时,我们不能告诉它停止,因为Applicatives 序列没有注意到早期的结果。
所以让我们进行最后一步实例化Monad
instance Monad GetC where
return = pure
Pure a >>= f = f a
GetC go >>= f = GetC (\char -> go char >>= f)
这让我们可以立即实现停止getAll
getLn :: GetC String
getLn = getc >>= \c -> case c of
'\n' -> return []
s -> fmap (s:) getLn
或者,使用do 表示法
getLn :: GetC String
getLn = do
c <- getc
case c of
'\n' -> return []
s -> fmap (s:) getLn
那是什么?为什么我们现在可以编写一个停止循环?
因为(>>=) :: m a -> (a -> m b) -> m b 让第二个参数,一个纯值的函数,选择下一个动作,m b。在这种情况下,如果传入字符是'\n',我们选择return [] 并终止循环。如果不是,我们选择递归。
这就是为什么您可能需要Monad 而不是Functor。故事还有很多,但这些都是基础。