【问题标题】:Using return vs. not using return in the list monad在列表单子中使用 return 与不使用 return
【发布时间】:2012-07-04 14:47:18
【问题描述】:

我开始了我的 Grand Haskell Crusade (GHC :) ),我对 monads 和 IO 函数有点困惑。谁能简单解释一下这两个函数有什么区别?

f1 = do x <- [1,2]
        [x, x+1] -- this is monad, right?

f2 = do x <- [1,2]
        return [x, x+1]

结果是:

*Main> f1
[1,2,2,3]

*Main> f2
[[1,2],[2,3]]

【问题讨论】:

  • 除了这些出色的答案之外,我想指出的是,您不能将Monad 类中的return 与类C 命令式语言中的return 关键字混淆。它们完全不同,但很多习惯于这些语言的人看到return 并没有意识到它不会导致返回值,而是进行计算以返回该值在稍后进行评估时。作为一种函数式语言,Haskell 不需要任何类似于 return 语句的东西。
  • 确实,Haskell 选择使用 return 这个词,恕我直言,是一个巨大的错误。但由于向后兼容,现在没有回头路了!
  • @DanBurton: 你总是可以做一个别名:) do_not_return a = return a
  • 已经有一个别名适用于大多数 Monad 类型:pure
  • @JesseHallett "pure" 也是一个坏名字。 pure x 意味着采用 x 并使其“纯”(如 sin x 采用 x 并返回 x 的正弦)。但实际上恰恰相反:它需要一个“纯”x

标签: list haskell monads do-notation


【解决方案1】:

要了解为什么会出现特定的答案,脱糖的解释非常有帮助。让我用一些关于发展对 Haskell 代码的看法的一般性建议来补充它们。

Haskell 的类型系统没有区分两个可分离的“道德”目的:

  • [x] values 的类型,它们是从 x 中提取的元素的列表
  • [x] x 元素的计算类型,允许优先选择

这两个概念具有相同表示的事实并不意味着它们扮演相同的角色。在f1 中,[x, x+1] 扮演着计算的角色,因此它产生的可能性被合并到整个计算产生的选择中:这就是列表单子的&gt;&gt;= 所做的。然而,在f2 中,[x, x+1] 扮演着值的角色,因此整个计算会在两个值(恰好是列表值)之间产生一个优先选择。

Haskell 不使用类型来进行这种区分 [你现在可能已经猜到我认为应该这样做,但那是另一回事了]。相反,它使用语法。所以你需要在阅读代码时训练你的头脑去感知价值和计算的角色。 do 表示法是一种用于构造计算的特殊语法。 do 中的内容由以下模板工具包构建:

三个蓝色部分构成do-computations。我已将计算孔标记为蓝色,将值孔标记为红色。这并不意味着是一个完整的语法,只是一个指导如何在你的脑海中感知代码片段。

确实,您可以在蓝色位置编写任何旧表达式,前提是它具有适当的一元类型,并且生成的计算将根据需要使用&gt;&gt;= 合并到整体计算中。在您的 f1 示例中,您的列表位于蓝色位置并被视为优先选择。

同样,你可以在红色的地方写表达式,这些地方很可能有单子类型(比如本例中的列表),但它们将被视为相同的值。这就是f2 中发生的情况:实际上,结果的外括号是蓝色的,但内括号是红色的。

在阅读代码时训练您的大脑进行值/计算分离,以便您本能地知道文本的哪些部分在做什么。一旦你重新编程了你的脑袋,f1f2 之间的区别似乎完全正常!

【讨论】:

  • 关于“Haskell 不使用类型来进行这种区分[你现在可能已经猜到我认为它应该......”:当然,monad 概念的部分要点是这些类型无论如何,我们都在到处闲逛,比如 list、maybe 等,碰巧也是 monad,因为它们支持正确的操作,因此可以用作 monad 和容器类型。你是不是在提倡我们不要让 list 成为 monad,而是发明一种与 list 相同的新类型并将其变成 monad,这样我们就不能混合正常的 list 操作和 monad 操作?
  • @Ben 是的,这似乎是他所提倡的。我们确实制作了newtype Ziplist a ...,为什么不对称?除了普通的concatMap 之外,还有几种bind 实现可能,比如在流之间交替的(`bind_i`f) = foldr (++/) [] . fmap f 其中(x:xs) ++/ ys = x:ys ++/ xs。我们也必须newtype,那为什么不对称呢?这似乎是论据。我怀疑在更深层次上,列表是具体类型和抽象类型之间的区别。我们可以在平衡二叉树上实现列表,并且必须复制 monad 定义代码。
  • @WillNess 确实,最多可以在本机列表上直接使用列表 monad 的一个“版本”,因此该“默认”实例和额外的 newtype 实例之间存在不对称。我们可以通过总是创建一个新类型来避免这种情况,但这与 monad 无关,所以我们可能应该对所有类型类都这样做,这对我来说听起来很痛苦。
  • @WillNess 这似乎是我所提倡的?我很小心,没有说出我所提倡的。请放心,我并不是在提倡你建议的我可能是什么。问题是如何将类型划分为“效果权限”和“值类型”。 Haskell 允许两种方式来做到这一点:在红色的地方,唯一的效果是偏性,值部分是整个类型;在蓝色的地方,效果部分应用于值部分。术语的语法选择使用哪种类型分析。我宁愿扩展类型的语法来标记效果/值的边界,但是以一种可重新协商的方式。
  • @WillNess 不用担心。我的实验语言,弗兰克,更接近我的想法。它允许您在本地更改解释正常应用语法的“环境单子”。不需要return 或从驯服的单子显式提升到狂野的单子,但是有显式的thunking(暂停计算作为值)和强制(运行暂停的计算)。这很有趣,但可能很难改造。
【解决方案2】:

这里的其他答案是正确的,但我想知道它们是否不是您所需要的......我会尽量保持简单,只有两点:


第 1 点。return 在 Haskell 语言中并不是什么特别的东西。它不是关键字,也不是其他东西的语法糖。它只是Monad 类型类中的一个函数。它的签名很简单:

return :: a -> m a

m 是我们当时谈论的任何一个 monad。它需要一个“纯”值并将其塞入您的 monad。 (顺便说一句,还有另一个函数叫做pure,它基本上是return的同义词......我更喜欢它,因为名字更明显!)无论如何,如果m是列表monad,那么return有这个类型:

return :: a -> [a]

如果有帮助,您可以考虑类型同义词type List a = [a],这可能会让List 是我们替换m 的东西更加明显。无论如何,如果您要自己实现return,那么实现它的唯一合理方法是获取一些值(无论a 的任何类型)并将其单独粘贴在一个列表中:

return a = [a]

所以我可以在列表单子中说return 1,我会得到[1]。我也可以说return [1, 2, 3],我会得到[[1, 2, 3]]


第 2 点。IO 是一个 monad,但并非所有 monad 都是 IO 许多 Haskell 教程似乎主要出于历史原因将这两个主题混为一谈(顺便说一下,同样令人困惑的历史原因这导致return 的名字如此糟糕)。听起来您可能对此有一些(可以理解的)困惑。

在您的代码中,您在 monad 列表中,因为您编写了 do x &lt;- [1, 2]。例如,如果您写了do x &lt;- getLine,您将在IO monad 中(因为getLine 返回IO String)。无论如何,你在 list monad 中,所以你得到了 list 对上面描述的 return 的定义。您还可以得到列表对&gt;&gt;= 的定义,它只是concatMap 的(翻转版本),定义为:

concatMap :: (a -> [b]) -> [a] -> [b]
concatMap f xs = concat (map f xs)

其他发布的答案几乎都涵盖了这里 :) 我知道我没有直接回答您的问题,但我希望这两点能够解决您可能会感到困惑的基本问题。

【讨论】:

    【解决方案3】:

    bindreturn重写代码时很容易看出:

    [1,2] >>= (\x->        [x,x+1]) === concatMap (\x-> [  x,x+1  ]) [1,2]
    
    [1,2] >>= (\x-> return [x,x+1]) === concatMap (\x-> [ [x,x+1] ]) [1,2]
    

    您的第一个代码相当于在第二个代码的结果上调用join,删除了return :: a -&gt; m a 引入的一个单子“层”,合并 "list" 正在使用的 monad,以及您的值的 "list"。例如,如果您要退回一对,那么省略 return 就没有多大意义:

                                         -- WRONG: type mismatch
    [1,2] >>= (\x->        (x,x+1)) === concatMap (\x-> (  x,x+1  )) [1,2]
                                         -- OK:
    [1,2] >>= (\x-> return (x,x+1)) === concatMap (\x-> [ (x,x+1) ]) [1,2]
    

    或者,我们可以使用join/fmap 重写:

    ma >>= famb === join (fmap famb ma)   -- famb :: a -> m b, m ~ []
    
    join (fmap (\x->        [x,x+1]) [1,2]) = concat [ [  x,x+1  ] | x<-[1,2]]
    join (fmap (\x->        (x,x+1)) [1,2]) = concat [ (  x,x+1  ) | x<-[1,2]]  -- WRONG
    join (fmap (\x-> return [x,x+1]) [1,2]) = concat [ [ [x,x+1] ] | x<-[1,2]]
    
                                                             =  [y | x<-[1,2], y<-[ x,x+1 ]]
                                                {- WRONG -}  =  [y | x<-[1,2], y<-( x,x+1 )]
                                                             =  [y | x<-[1,2], y<-[[x,x+1]]]
    

    【讨论】:

    • 第一个代码,根据 Monad 定律,相当于 do {x &lt;- [1,2]; r &lt;- [x, x+1]; return r},根据 Monad 理解,写成 [ r | x &lt;- [1,2], r &lt;- [x,x+1] ];第二个是[ [x,x+1] | x &lt;- [1,2] ]。所以,f1 = flip ($) &lt;$&gt; [1,2] &lt;*&gt; [id,(1+)]f2 = (\x-&gt;[x,x+1]) &lt;$&gt; [1,2]。 -- 使用答案中的元组,我们得到flip ($) &lt;$&gt; [1,2] &lt;*&gt; (id,(1+)) (wrong) 和(\x-&gt;(x,x+1)) &lt;$&gt; [1,2] (OK)。
    【解决方案4】:

    List 类型 ([]) 是一个 monad,是的。

    现在,记住return 的作用。这从它的类型签名很容易看出:return :: Monad m =&gt; a -&gt; m a。让我们将列表类型替换为:return :: a -&gt; [a]。所以这个函数接受一些值并只返回该值的列表。相当于\ x -&gt; [x]

    所以在第一个代码示例中,最后有一个列表:[x, x+1]。在第二个示例中,您有一个嵌套 列表:一个列表来自[x, x + 1]另一个 列表来自return。在这种情况下,return [x, x + 1] 行可以重写为 [[x, x + 1]]

    最后,结果是所有可能结果的列表。也就是说,我们将x 的结果连接为1,将x 的结果连接为2(感谢x &lt;- [1,2] 行)。所以在第一种情况下,我们连接两个列表;在第二种情况下,我们连接两个列表列表,因为额外的return 将结果包装在一个额外的列表中。

    【讨论】:

      【解决方案5】:

      do 语法脱糖为等价物

      f1 = [1,2] >>= \x -> [x, x+1]
      f2 = [1,2] >>= \x -> return [x, x+1]
      

      现在,&gt;&gt;= 来自 Monad 类,

      class Monad m where
          (>>=) :: m a -> (a -> m b) -> m b
          return :: a -> m a
      

      f1f2&gt;&gt;= 的LHS 是[a](其中a 默认为Integer),所以我们真的在考虑

      instance Monad [] where
          (>>=) :: [a] -> (a -> [b]) -> [b]
          ...
      

      这遵循与 monad laws 相同的单子,但与,

      instance Monad IO where ...
      

      &gt;&gt;= 用于其他单子,所以不要盲目地将你所知道的应用于另一个,好吗? :)

      instance Monad [] 在 GHC 中是这样定义的

      instance Monad [] where
          m >>= k = foldr ((++) . k) [] m
          return x = [x]
          ...
      

      []&gt;&gt;=可能更容易理解为

      instance Monad [] where
          m >>= k = concatMap k m
      

      如果你把它应用到原件上,你会得到

      f1 = concatMap (\x -> [x, x+1]) [1,2]
      f2 = concatMap (\x -> [[x, x+1]]) [1,2]
      

      很清楚为什么f1f2 的值是它们的值。

      【讨论】:

        【解决方案6】:

        我的理解是这样做

        在 List monad 中返回 [1,2] 和做的一样

        func :: Maybe (Maybe Int)
        func = return $ Just 1
        

        这就是你最终得到包装列表的原因,因为 [] 只是语法糖,对吗?

        当他真正想做的时候

        func :: Maybe Int
        func = return 5
        

        func = Just 5
        

        我认为可能更容易看到 Maybe monad 发生了什么。

        所以当你这样做时

        return [1,2]
        

        你和你做的一样

        [ [1,2] ]
        

        【讨论】:

        • return Just 1 解析为 (return Just) 1 不起作用:(
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-02-29
        • 2020-08-11
        • 2019-08-18
        • 1970-01-01
        • 2014-03-15
        相关资源
        最近更新 更多