关于>> = Monad运算符的签名

cib*_*en1 9 monads haskell

这是Haskell中众所周知的>> =运算符的签名

>>= :: Monad m => m a -> (a -> m b) -> m b
Run Code Online (Sandbox Code Playgroud)

问题是为什么函数的类型是

(a -> m b)
Run Code Online (Sandbox Code Playgroud)

代替

(a -> b)
Run Code Online (Sandbox Code Playgroud)

我会说后者更实用,因为它允许在定义的monad中直接集成现有的"纯"函数.

相反,编写一般的"适配器"似乎并不困难

adapt :: (Monad m) => (a -> b) -> (a -> m b)
Run Code Online (Sandbox Code Playgroud)

但无论如何,我认为更可能是你已经有了(a -> b)替代(a -> m b).

注意.我通过"实际"和"可能"来解释我的意思.如果你还没有在程序中定义任何monad,那么,你拥有的函数是"纯粹的" (a -> b),你将拥有0类型的函数,(a -> m b)因为你还没有定义m.如果那时你决定定义一个monad,m那就需要a -> m b定义新的函数.

Pet*_*lák 9

原因是(>>=)更一般.您建议的功能被调用liftM,可以很容易地定义为

liftM :: (Monad m) => (a -> b) -> (m a -> m b)
liftM f k  =  k >>= return . f
Run Code Online (Sandbox Code Playgroud)

这个概念有一个称为自己的类型类Functorfmap :: (Functor m) => (a -> b) -> (m a -> m b).每个Monad都是一个Functorwith fmap = liftM,但由于历史原因,它还没有()在类型层次结构中捕获.

adapt你的建议可以定义为

adapt :: (Monad m) => (a -> b) -> (a -> m b)
adapt f = return . f
Run Code Online (Sandbox Code Playgroud)

注意,具有adapt相当于具有return作为return可以被定义为adapt id.

所以任何东西也>>=可以有这两个功能,但反之亦然.有结构Functors但不是Monads.

这种差异背后的直觉很简单:monad中的计算可能取决于先前monad的结果.重要的是(a -> m b)这意味着不仅仅是b,它的"效果" m b也可以依赖a.例如,我们可以拥有

import Control.Monad

mIfThenElse :: (Monad m) => m Bool -> m a -> m a -> m a
mIfThenElse p t f = p >>= \x -> if x then t else f
Run Code Online (Sandbox Code Playgroud)

但是Functor m使用just只能用约束来定义这个函数是不可能的fmap.函子只允许我们改变"内部"的值,但是我们不能"退出"来决定采取什么行动.


J. *_*son 7

基本上,(>>=)允许您对操作进行排序,以便后面的操作可以根据早期结果选择不同的行为.在Functor类类中可以使用类似于你要求的更纯粹的函数,并且可以使用(>>=)它来导出,但是如果你单独使用它就不再能够对操作进行排序.还有一个中间调用Applicative,允许您对操作进行排序,但不会根据中间结果更改它们.

举个例子,让我们构建一个简单的IO动作类型,从Functor到Applicative到Monad.


我们将重点关注GetC如下类型

GetC a = Pure a | GetC (Char -> GetC a)
Run Code Online (Sandbox Code Playgroud)

第一个构造函数将在时间上有意义,但第二个构造函数应该立即有意义 - GetC持有一个可以响应传入字符的函数.我们可以GetC变成一个IO动作来提供这些角色

io :: GetC a -> IO a
io (Pure a)  = return a
io (GetC go) = getChar >>= (\char -> io (go char))
Run Code Online (Sandbox Code Playgroud)

这清楚地说明了Pure它来自哪个---它处理我们类型中的纯值.最后,我们将提出GetC摘要:我们将不允许使用PureGetC直接使用,并允许我们的用户只访问我们定义的函数.我现在写下最重要的一个

getc :: GetC Char
getc = GetC Pure
Run Code Online (Sandbox Code Playgroud)

获得一个字符然后立即考虑的函数是纯值.虽然我把它称为最重要的功能,但很明显,现在它GetC是无用的.我们所能做的就是运行getc后跟io...来获得完全相同的效果getChar!

io getc        ===     getChar     :: IO Char
Run Code Online (Sandbox Code Playgroud)

但我们会从这里建立起来.


如开头所述,Functor类型类提供了与您正在寻找的函数完全相同的函数fmap.

class Functor f where
  fmap :: (a -> b) -> f a -> f b
Run Code Online (Sandbox Code Playgroud)

事实证明,我们可以实例化GetC,Functor所以让我们这样做.

instance Functor GetC where
  fmap f (Pure a)  = Pure (f a)
  fmap f (GetC go) = GetC (\char -> fmap f (go char))
Run Code Online (Sandbox Code Playgroud)

如果你眯着眼睛,你会发现它只会fmap影响Pure构造函数.在GetC构造函数中,它只是"被推下"并推迟到以后.这是一个关于弱点的暗示fmap,但让我们尝试一下.

io                       getc  :: IO Char
io (fmap ord             getc) :: IO Int
io (fmap (\c -> ord + 1) getc) :: IO Int
Run Code Online (Sandbox Code Playgroud)

我们已经能够修改我们IO对类型的解释的返回类型,但就是这样!特别是,我们仍然只能获得一个角色,然后回去IO做任何有趣的事情.

这是的弱点Functor.因为,正如你所指出的那样,它只涉及纯函数,它"卡在计算结束时" Pure仅修改构造函数.


接下来的步骤是Applicative,其延伸Functor这样

class Functor f => Applicative f where
  pure  :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b
Run Code Online (Sandbox Code Playgroud)

换句话说,它扩展了将纯值注入上下文允许纯函数应用程序跨越数据类型的概念.不出所料,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))
Run Code Online (Sandbox Code Playgroud)

Applicative允许我们对操作进行排序,并且可以从定义中清楚地看出.实际上,我们可以看到(<*>)向前推动字符应用程序,以便按顺序执行GetC任何一方的(<*>)操作.我们用Applicative这样的

fmap (,) getc <*> getc :: GetC (Char, Char)
Run Code Online (Sandbox Code Playgroud)

它使我们能够构建令人难以置信的有趣功能,而不仅仅是复杂功能Functor.例如,我们已经可以形成一个循环并获得无限的字符流.

getAll :: GetC [Char]
getAll = fmap (:) getc <*> getAll
Run Code Online (Sandbox Code Playgroud)

这证明了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)
Run Code Online (Sandbox Code Playgroud)

这使我们能够立即实施停止 getAll

getLn :: GetC String
getLn = getc >>= \c -> case c of
  '\n' -> return []
  s    -> fmap (s:) getLn
Run Code Online (Sandbox Code Playgroud)

或者,使用do符号

getLn :: GetC String
getLn = do
  c <- getc
  case c of
    '\n' -> return []
    s    -> fmap (s:) getLn
Run Code Online (Sandbox Code Playgroud)

什么给出了什么?为什么我们现在可以写一个停止循环?

因为(>>=) :: m a -> (a -> m b) -> m b让第二个参数,纯值的函数,选择下一个动作,m b.在这种情况下,如果传入的字符是'\n'我们选择return []并终止循环.如果没有,我们选择递归.

所以这就是为什么你可能想要Monad超过a Functor.这个故事还有很多,但这些都是基础知识.