Haskell中的Monoids和Num

时间:2017-11-30 15:10:35

标签: haskell monoids

在过去的几个月里,我一直在学习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如何知道在它折叠时使用的Monoid用于mappend?因为默认情况下,树中的数字只是Num类型,我们从未明确地说过它们在某些Monoid中的位置,例如Sum或Product。

那么GHCi如何推断使用正确的Monoid?或者我现在完全离开了?

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

3 个答案:

答案 0 :(得分:18)

简短回答:它是foldMap签名中的类型约束。

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

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

这意味着,如果我们声明Tree成员Foldable(不是Tree* -> *),则表示foldMap是在该树上定义,例如:foldMap :: Monoid m => (a -> m) -> Tree a -> m。因此,这意味着结果类型(以及传递给foldMapm的函数的结果必须是Monoid

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

现在Int不是Monoid的实例。你在这里使用F.foldl (+) 0 testTree,这意味着你或多或少地构建了一个" ad hoc"独异。如果我们查看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 -> b -> a,则此函数的类型为b -> 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。)

所以这里发生的是mempty中使用的foldMapmempty = 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,我们将执行上述mappend。所以" 专业" foldMap将如下所示:

-- 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被替换为无操作(身份函数)。

答案 1 :(得分:9)

它不需要。 foldl被翻译为foldr,转换为foldMap超过Endo,这意味着功能组合,这意味着您提供的函数的简单嵌套。< / p>

或者其他什么。意思是,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) <> Dual (Endo g) = Dual (Endo g <> Endo f) = Dual (Endo (g . f))。所以当appEndo打击时,已构建的函数链,即

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

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

                                                 0

需要注意的另一件事是EndoDualnewtype s,因此所有这些诡计都将由编译器完成,并且在运行时间之后消失。

答案 2 :(得分:4)

(隐式地,如果不是显式的),a -> a形式的普通函数的monoid实例,其中mappend对应于函数组合,mempty对应于{{ 1}}功能。

什么是id?它是一个函数(+)。如果您使用(Num a) => a -> a -> a覆盖了可折叠的全部数字foldMap,则可以将每个数字转换为部分应用的+,即(+ <some number)。瞧,你已经找到了将你可折叠的一切变成幺半群的魔法a -> a

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

f

,这将生成一个终极foldMap (+) [1, 2, 3, 4] ,您可以将其应用于(Num a) => a -> a以获取0

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

a -> a

这里Prelude Data.Monoid> (appEndo (foldMap (Endo . (+)) [1, 2, 3, 4])) 0 10 只是解除普通Endo .的烦恼,因此他们拥有自然a -> a个实例。完成Monoid之后,通过将所有内容都转换为foldMap并将它们与组合链接在一起来减少我们的可折叠性,我们使用a -> a提取最终a -> a,最后将其应用于appEndo