【问题标题】:On the signature of >>= Monad operator关于 >>= Monad 运算符的签名
【发布时间】:2014-01-19 19:43:39
【问题描述】:

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

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

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

(a -> m b)

而不是

(a -> b)

我会说后一种更实用,因为它允许在定义的 monad 中直接集成现有的“纯”函数。

相反,写一个通用的“适配器”似乎并不难

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

但无论如何,我认为您已经拥有(a -> b) 而不是(a -> m b) 的可能性更大。

注意。我解释了“实际”和“可能”的含义。 如果您还没有在程序中定义任何 monad,那么,您拥有的函数是“纯”(a -> b) 并且您将拥有 0 个 (a -> m b) 类型的函数,因为您还没有定义 m。如果那时您决定定义一个 monad m,则需要定义新的 a -> m b 函数。

【问题讨论】:

  • fmap :: Functor f => (a -> b) -> f a -> f b
  • 简短的回答是>>=fmap 更强大,所以有时(通常)你需要 >>=fmap 不够好.

标签: haskell monads


【解决方案1】:

原因是(>>=)更通用。您建议的函数称为liftM,可以轻松定义为

liftM :: (Monad m) => (a -> b) -> (m a -> m b)
liftM f k  =  k >>= return . f

这个概念有自己的类型类,称为Functorfmap :: (Functor m) => (a -> b) -> (m a -> m b)。每个Monad 也是Functorfmap = liftM,但由于历史原因,这不是(yet)类型类层次结构中捕获的。

你建议的adapt可以定义为

adapt :: (Monad m) => (a -> b) -> (a -> m b)
adapt f = return . f

请注意,拥有adapt 等同于拥有return,因为return 可以定义为adapt id

所以任何具有>>= 的东西也可以具有这两个功能,但反之则不行。 There are structures that are Functors but not 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

但不能仅使用Functor m 约束来定义此函数,仅使用fmap。函子只允许我们改变“内部”的值,但我们不能把它“拿出”来决定采取什么行动。

【讨论】:

    【解决方案2】:

    基本上,(>>=) 允许您对操作进行排序,以便后面的操作可以根据早期的结果选择不同的行为。 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 抽象化:我们将禁止直接使用PureGetC,只允许我们的用户访问我们定义的函数。我现在就写最重要的一篇

    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 允许我们对操作进行排序,这在定义中可能已经很清楚了。事实上,我们可以看到(&lt;*&gt;) 将字符应用程序向前推动,以便(&lt;*&gt;) 两侧的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
    

    那是什么?为什么我们现在可以编写一个停止循环?

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

    这就是为什么您可能需要Monad 而不是Functor。故事还有很多,但这些都是基础。

    【讨论】:

      【解决方案3】:

      正如其他人所说,您的绑定是 Functor 类的 fmap 函数,也就是 &lt;$&gt;

      但为什么它不如&gt;&gt;= 强大?

      写一个通用的“适配器”似乎并不难

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

      你确实可以用这种类型写一个函数:

      adapt f x = return (f x)
      

      然而,这个函数不能做我们可能希望&gt;&gt;= 的参数做的所有事情。 adapt 无法产生有用的值。

      在列表 monad 中,return x = [x],所以adapt 将始终返回一个单元素列表。

      Maybe monad 中,return x = Some x,所以adapt 永远不会返回None

      IO monad 中,一旦你检索到一个操作的结果,你所能做的就是从中计算一个新的值,你不能运行后续的操作!

      等等。所以简而言之,fmap 能够做的事情比&gt;&gt;= 少。这并不意味着它没有用——如果它是的话,它就没有名字了:) 但它的功能不那么强大。

      【讨论】:

        【解决方案4】:

        monad 的全部“要点”实际上(将其置于函子或应用程序之上)是您可以根据左侧的值/结果确定您“返回”的 monad。

        例如,Maybe 类型上的&gt;&gt;= 允许我们决定返回Just xNothing。您会注意到,使用函子或应用程序,不可能根据“排序的”Maybe“选择”返回Just xNothing

        尝试实现类似的东西:

        halve :: Int -> Maybe Int
        halve n | even n    = Just (n `div` 2)
                | otherwise = Nothing
        
        return 24 >>= halve >>= halve >>= halve
        

        只有&lt;$&gt; (fmap1) 或&lt;*&gt; (ap)。

        实际上,您提到的“纯代码的直接集成”是functor design pattern 的一个重要方面,并且非常有用。但是,它在很多方面与 &gt;&gt;= 背后的动机无关——它们适用于不同的应用程序和事物。

        【讨论】:

          【解决方案5】:

          我有一段时间有同样的问题,并在想,一旦将a -&gt; b 映射到m a -&gt; m b 看起来更自然,为什么还要打扰a -&gt; m b。这类似于问“为什么我们需要一个给定函子的 monad”。

          我给自己的小答案是,a -&gt; m b 考虑了副作用或其他复杂性,您无法使用函数 a -&gt; b

          更好的措辞形式here(强烈推荐):

          monadic value M a 本身可以看作是一种计算。 Monadic 函数表示在某种程度上是非标准的计算,即编程语言不自然地支持。这可能意味着纯函数式语言中的副作用或非纯函数式语言中的异步执行。普通函数类型无法对此类计算进行编码,而是使用具有一元结构的数据类型对其进行编码。

          我会强调普通函数类型不能编码这样的计算,其中普通是a -&gt; b

          【讨论】:

            【解决方案6】:

            我认为 J. Abrahamson 的回答指出了正确的原因:

            基本上,(>>=) 可以让您对操作进行排序,以便后面的操作可以根据前面的结果选择不同的行为。 Functor 类型类中提供了您要求的更纯粹的函数,并且可以使用 (>>=) 派生,但如果您只使用它您将无法再对操作进行排序

            让我展示一个针对&gt;&gt;= :: Monad m =&gt; m a -&gt; (a -&gt; b) -&gt; m b 的简单反例。

            很明显,我们希望将值绑定到上下文。也许我们需要按顺序链接函数在这些“上下文值”上。 (这只是 Monads 的一个用例)。

            Maybe简单地作为“上下文价值”的一个例子。

            然后定义一个“假”的 monad 类:

            class Mokad m where  
                 returk :: t -> m t  
                 (>>==) :: m t1 -> (t1 -> t2) -> m t2
            

            现在让我们尝试让Maybe 成为Mokad 的一个实例

            instance Mokad Maybe where
                     returk x = Just x
                     Nothing >>== f = Nothing
                     Just x >>== f = Just (f x) -- ????? always Just ?????
            

            出现第一个问题:&gt;&gt;== 总是返回Just _

            现在让我们尝试使用&gt;&gt;== 将函数链接到Maybe (我们依次提取三个Maybes的值只是为了添加它们)

            chainK :: Maybe Int -> Maybe Int -> Maybe Int -> Maybe Int
            chainK ma mb mc = md 
                  where
                    md = ma >>== \a -> mb >>== \b -> mc >>== \c -> returk $ a+b+c
            

            但是,这段代码无法编译:md 类型为Maybe (Maybe (Maybe Int)),因为每次使用&gt;&gt;== 时,都会将之前的结果封装到Maybe 框中。

            相反,&gt;&gt;= 工作正常:

            chainOK :: Maybe Int -> Maybe Int -> Maybe Int -> Maybe Int
            chainOK ma mb mc = md 
                  where
                    md = ma >>= \a -> mb >>= \b -> mc >>= \c -> return (a+b+c)  
            

            【讨论】:

            • 否决票是否意味着 J. Abrahamson 的错误?还是我的例子错了?
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2011-12-19
            • 2016-10-20
            • 2016-04-30
            • 1970-01-01
            相关资源
            最近更新 更多