【问题标题】:Can "bind" do a reduce on a List monad?“绑定”可以在 List monad 上做一个 reduce 吗?
【发布时间】:2012-06-12 23:12:47
【问题描述】:

我知道如何使用仅使用“绑定”操作的 list monad 来执行与 Scheme(或 Python)的 mapfilter 函数等效的操作。

这里有一些 Scala 来说明:

scala> // map
scala> List(1,2,3,4,5,6).flatMap {x => List(x * x)}                        
res20: List[Int] = List(1, 4, 9, 16, 25, 36)

scala> // filter    
scala> List(1,2,3,4,5,6).flatMap {x => if (x % 2 == 0) List() else List(x)}
res21: List[Int] = List(1, 3, 5)

在 Haskell 中也是一样的:

Prelude> -- map
Prelude> [1, 2, 3, 4, 5, 6] >>= (\x -> [x * x])
[1,4,9,16,25,36]

Prelude> -- filter
Prelude> [1, 2, 3, 4, 5, 6] >>= (\x -> if (mod x 2 == 0) then [] else [x])
[1,3,5]

Scheme 和 Python 也有一个 reduce 函数,它通常与 mapfilter 组合在一起。 reduce 函数使用提供的二元函数组合列表的前两个元素,然后将结果组合为下一个元素,依此类推。计算值列表的总和或乘积的常见用途。这里有一些 Python 来说明:

>>> reduce(lambda x, y: x + y, [1,2,3,4,5,6])
21
>>> (((((1+2)+3)+4)+5)+6)
21

有没有办法只使用列表单子上的绑定操作来做这个reduce 的等效操作?如果 bind 不能自己执行此操作,那么执行此操作的最“单子”方式是什么?

如果可能,请在回答时限制/避免使用语法糖(即:Haskell 中的 do 符号或 Scala 中的序列推导)。

【问题讨论】:

    标签: functional-programming monads


    【解决方案1】:

    绑定操作的定义属性之一是结果仍然在 monad¹“内部”。因此,当您对列表执行绑定时,结果将再次成为列表。由于reduce操作²通常会产生列表以外的东西,因此不能用绑定操作来表示。

    除此之外,列表上的绑定操作(即 concatMap/flatMap)一次只查看一个元素,并且无法重用前面步骤的结果。因此,即使我们可以将结果包装在单元素列表中,也无法仅使用 monad 操作来做到这一点。


    ¹因此,如果您的类型允许您对其执行除 monad 类型类定义的操作之外的任何操作,则您永远无法“突破” monad。这就是 IO monad 起作用的原因。

    ² 顺便说一句,在 Haskell 和 Scala 中称为 fold。

    【讨论】:

    • 我意识到 bind 不能“突破”单子。我假设必须有一个不使用绑定的额外“解包”步骤。也就是说,直到那个展开步骤,[1,2,3,4,5,6] 的结果和+ 的一些转换将是[21]。有没有什么办法可以通过绑定加上一个最终的“展开”操作来实现减少/折叠?
    • 感谢您让我知道它在 Haskell 和 Scala 中被称为“折叠”。
    • @LaurenceGonsalves 啊,我明白了。但这仍然是不可能的。 concatMap/>>= 一次只查看一个元素,并没有为您提供访问以前结果的方法,因此无法像折叠中那样拥有累加器。
    • 谢谢。这就是我的想法,但是对于 monads 非常陌生,我不确定是否缺少一些聪明的技巧。
    【解决方案2】:

    如果 bind 不能自己执行此操作,那么执行此操作的最“单子”方式是什么?

    虽然@sepp2k 给出的答案是正确的,但有一种方法可以对列表进行类似reduce 的操作,但使用product 或“writer” monad 和对应于 distributing 的操作 list functor之上的product monad

    定义是:

    import Control.Monad.Writer.Lazy
    import Data.Monoid
    
    reduce :: Monoid a => [a] -> a
    reduce xs = snd . runWriter . sequence $ map tell xs 
    

    让我打开包装:

    • Writer monad 的数据类型为 Writer w a,它基本上是值 a 和“写入”值 w 的元组(乘积)。 w 写入值的类型必须是 monoid,其中 Writer monad 的 bind 操作定义如下:

          (w, a) >>= f = let (w', b) = f a in (mappend w w', b)
      

      即取传入的写入值和结果写入值,并使用 monoid 的二元运算将它们组合起来。

    • tell 操作写入一个值 tell :: w -> Writer w ()。因此map tell 具有[a] -> [Writer a ()] 类型,即一个单子值列表,其中原始列表的每个元素都已“写入”到单子中。

    • sequence :: Monad m => [m a] -> m [a] 对应于列表和 monad 之间的分配律,即在列表类型上分配 monad 类型; sequence 可以在绑定方面定义为:

      sequence [] = return []
      sequnece (x:xs) = x >>= (\x' -> (sequence xs) >>= (\xs' -> return $ x':xs'))
      

      actually the implementation in Prelude uses foldr,类似归约用法的线索)

      因此,sequence $ map tell xs 的类型为 Writer a [()]

    • runWriter 操作解包 Writer 类型,runWriter :: Writer w a -> (a, w), 在这里和snd组成,用来投影出累计值。

    Ints 列表的一个示例用法是使用 monoid 实例:

      instance Monoid Int where
                  mappend = (+)
                  mempty = 0
    

    然后:

      > reduce ([1,2,3,4]::[Int])
      10
    

    【讨论】:

    • 我怀疑这是我在实践中曾经使用过的东西,但这是一本有趣/有教育意义的读物!谢谢。
    猜你喜欢
    • 2021-03-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多