【问题标题】:Monoids and Num in HaskellHaskell 中的 Monoids 和 Num
【发布时间】:2018-05-14 13:39:30
【问题描述】:

过去几个月我一直在学习 Haskell,遇到了一个令我困惑的 Monoids 示例。

鉴于这些定义:

data Tree a = Empty | Node a (Tree a) (Tree a) deriving (Show, Read, Eq) 

instance F.Foldable Tree where  
    foldMap f Empty = mempty  
    foldMap f (Node x l r) = F.foldMap f l `mappend`  
                             f x           `mappend`  
                             F.foldMap f r  

还有这棵树:

testTree = Node 5  
        (Node 3  
            (Node 1 Empty Empty)  
            (Node 6 Empty Empty)  
        )  
        (Node 9  
            (Node 8 Empty Empty)  
            (Node 10 Empty Empty)  
        )  

如果我跑:

ghci> F.foldl (+) 0 testTree  
42  
ghci> F.foldl (*) 1 testTree  
64800  

GHCi 如何知道 mappend 在折叠时使用什么 Monoid?因为默认情况下,树中的数字只是 Num 类型,我们从未明确指出它们是 Sum 或 Product 等 Monoid 的位置。

那么 GHCi 如何推断要使用的正确 Monoid 呢?还是我现在完全不在了?

示例来源:http://learnyouahaskell.com/functors-applicative-functors-and-monoids#monoids

【问题讨论】:

  • 虽然它不是以这种方式实现的,但您可以假设F.foldl 在内部选择列表幺半群,将树变成列表,然后像往常一样折叠它。只要树是有限的,这个心智模型应该能准确地描述实际结果。
  • 实际上SumProduct 半群模型在这里没有用到。

标签: haskell monoids


【解决方案1】:

不需要。 foldl 被翻译成 foldr,后者在 Endo 上翻译成 foldMap,这意味着函数组合,这意味着您提供的函数的简单嵌套

什么的。意思是,foldl 可以翻译成 foldMap 而不是 Dual . Endo 组成从左到右等。

更新: 是的,the docs says

可折叠实例应满足以下规则:

foldr f z t = appEndo (foldMap (Endo . f) t ) z
foldl f z t = appEndo (getDual (foldMap (Dual . Endo . flip f) t)) z  -- << --
fold = foldMap id

Dual (Endo f) &lt;&gt; Dual (Endo g) = Dual (Endo g &lt;&gt; Endo f) = Dual (Endo (g . f))。因此,当appEndo 触发时,已构建的函数链,即

    ((+10) . (+9) . (+8) . (+5) . ... . (+1))

或等效项(此处显示为 (+) 案例),应用于用户提供的值 - 在您的案例中,

                                                 0

另外需要注意的是EndoDualnewtypes,所以所有这些阴谋将由编译器完成,并在运行时消失。

【讨论】:

  • 但是在某些时候节点中存在的 Num 需要是某种 Monoid,否则 mappend 会抛出异常,不是吗?
  • 从头开始,Endo 是一个幺半群,不过我以前没见过。
  • 不,他们不是。 mappend 适用于 Endo f,它是 Endo f &lt;&gt; Endo g = Endo (f . g)。这里的“Endo”只是一个标记;重要的是f . g
  • appEndo 实际上不会导致应用任何内容。它只是从Endo newtype 包装器中提取幺半折叠的最终函数。您仍然必须手动将其应用于某些东西,这就是 z 参数。
  • @AsadSaeeduddin 就像($)。它是否应用其参数功能? (否)我们认为它是应用程序运算符吗? (是的)。
【解决方案2】:

简答:这是foldMap签名中的类型约束。

如果我们查看Foldable(更具体地说是foldMap)的源代码,我们会看到:

class Foldable (t :: * -> *) where
  ...
  foldMap :: Monoid m => (a -> m) -> t a -> m

这意味着如果我们声明TreeFoldable 的成员(不是Tree 具有类型* -&gt; *),这意味着在该树上定义了foldMap,例如:@987654331 @。所以这意味着结果类型(以及传递给foldMap的函数的结果)m必须是Monoid

Haskell 是静态类型的:在编译之后,Haskell 准确地知道每个函数 instance 中传入的类型。这意味着它知道例如输出类型是什么,以及如何处理它。

现在Int 不是Monoid 的实例。你在这里使用F.foldl (+) 0 testTree,这意味着你或多或少地构建了一个“临时”幺半群。如果我们查看source code of foldl,则此方法有效:

foldl :: (b -> a -> b) -> b -> t a -> b
foldl f z t = appEndo (getDual (foldMap (Dual . Endo . flip f) t)) z

这有很多围绕参数fzt 的逻辑。所以让我们先分解一下。

让我们先来看看Dual . Endo . flip f。简称:

helper = \x -> Dual (Endo (\y -> f y x))

DualEndo 是每个构造函数都接受一个参数的类型。所以我们将f y x 的结果包装在Dual (Endo ...) 构造函数中。

我们将把它用作foldMap 的函数。如果我们的f 的类型为a -&gt; b -&gt; a,那么这个函数的类型为b -&gt; Dual (Endo a)。所以传递给foldMap的函数的输出类型为Dual (Endo a)。现在,如果我们检查源代码,我们会看到两件有趣的事情:

instance Monoid (Endo a) where
        mempty = Endo id
        Endo f `mappend` Endo g = Endo (f . g)

instance Monoid a => Monoid (Dual a) where
        mempty = Dual mempty
        Dual x `mappend` Dual y = Dual (y `mappend` x)

(注意是y `mappend` x,不是x `mappend` y)。

所以这里发生的是foldMap 中使用的memptymempty = Dual mempty = Dual (Endo id)。所以一个封装了身份函数Dual (Endo ...)

此外,两个对偶的mappend 归结为Endo 中值的函数组合。所以:

mempty = Dual (Endo id)
mappend (Dual (Endo f)) (Dual (Endo g)) = Dual (Endo (g . f))

这意味着如果我们折叠树,如果我们看到Empty(一片叶子),我们将返回mempty,如果我们看到Node x l r,我们将执行@987654368 @ 如上所述。所以一个“specializedfoldMap 看起来像:

-- specialized foldMap
foldMap f Empty = Dual (Endo id)  
foldMap f (Node x l r) =  Dual (Endo (c . b . a))
        where Dual (Endo a) = foldMap f l
              Dual (Endo b) = helper x
              Dual (Endo c) = foldMap f l

因此,对于每个Node,我们在节点的子节点和项上从右到左创建一个函数组合。 ac 也可以是树的组合(因为它们是递归调用)。对于Leaf,我们什么都不做(我们返回id,但id 上的组合是无操作的)。

也就是说,如果我们有一棵树:

5
|- 3
|  |- 1
|  `- 6
`- 9
   |- 8
   `- 10

这将产生一个函数:

(Dual (Endo ( (\x -> f x 10) .
              (\x -> f x 9) .
              (\x -> f x 8) .
              (\x -> f x 5) .
              (\x -> f x 6) .
              (\x -> f x 3) .
              (\x -> f x 1)
            )
      )
)

(省略了身份,以使其更清晰)。这是getDual (foldMap (Dual . Endo . flip f)) 的结果。但是现在我们需要对这个结果进行后期处理。使用getDual,我们将内容包装在Dual 构造函数中。所以现在我们有:

Endo ( (\x -> f x 10) .
       (\x -> f x 9) .
       (\x -> f x 8) .
       (\x -> f x 5) .
       (\x -> f x 6) .
       (\x -> f x 3) .
       (\x -> f x 1)
     )

通过appEndo,我们得到包裹在Endo中的函数,所以:

( (\x -> f x 10) .
  (\x -> f x 9) .
  (\x -> f x 8) .
  (\x -> f x 5) .
  (\x -> f x 6) .
  (\x -> f x 3) .
  (\x -> f x 1)
)

然后我们将其应用于z“初始”值。这意味着我们将处理以z(初始元素)开头的链,并像这样应用它:

f (f (f (f (f (f (f z 1) 3) 6) 5) 8) 9) 10

所以我们构建了某种 Monoid,其中 mappendf 替换,mempty 作为无操作(身份函数)。

【讨论】:

    【解决方案3】:

    有(隐含的,如果不是显式的)普通函数的幺半群实例a -&gt; a,其中mappend 对应于函数组合,mempty 对应于id 函数。

    (+) 是什么?这是一个函数(Num a) =&gt; a -&gt; a -&gt; a。如果你用+foldMap 覆盖你的可折叠数字,你会将每个数字变成部分应用的(+ &lt;some number),即a -&gt; a。瞧,你发现了f 的魔力,它可以将你的可折叠设备中的所有东西都变成一个幺半群!

    假设函数有一个直接的 monoid 实例,您可以这样做:

    foldMap (+) [1, 2, 3, 4]
    

    ,这将产生最终的(Num a) =&gt; a -&gt; a,您可以将其应用到0 以获得10

    但是没有这样的直接实例,因此您需要使用内置的newtype 包装器Endo 和相应的解包器appEndo,它们为a -&gt; a 函数实现了monoid。这是它的样子:

    Prelude Data.Monoid> (appEndo (foldMap (Endo . (+)) [1, 2, 3, 4])) 0
    10
    

    这里Endo . 只是我们烦人的需要解除简单的a -&gt; as,以便他们拥有自然的Monoid 实例。在foldMap 完成通过将所有内容转换为a -&gt; as 并将它们与组合链接在一起来减少我们的可折叠之后,我们使用appEndo 提取最终的a -&gt; a,最后将其应用到0

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-12-25
      • 1970-01-01
      • 2021-12-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多