在过去的几个月里,我一直在学习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
答案 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
。因此,这意味着结果类型(以及传递给foldMap
)m
的函数的结果必须是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
这有很多围绕参数f
,z
和t
的逻辑。所以,让我们首先打破这一点。
我们先来看看Dual . Endo . flip f
。这是简短的:
helper = \x -> Dual (Endo (\y -> f y x))
Dual
和Endo
是每个构造函数带有一个参数的类型。因此,我们将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
中使用的foldMap
是mempty = 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
,我们在节点的子节点和项目上从右到左创建一个函数组合。 a
和c
也可以是树的组合(因为这些是递归调用)。如果是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,其中mappend
被f
取代,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
需要注意的另一件事是Endo
和Dual
是newtype
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
。