Haskell中的幺半群与Num
在过去的几个月里,我一直在学习Haskell,我遇到了一个令我困惑的幺半群的例子 鉴于这些定义:Haskell中的幺半群与Num,haskell,monoids,Haskell,Monoids,在过去的几个月里,我一直在学习Haskell,我遇到了一个令我困惑的幺半群的例子 鉴于这些定义: 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`
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
当mappend折叠时,GHCi如何知道使用哪个幺半群?因为在默认情况下,树中的数字只是Num类型,我们从未明确表示它们是某些幺半群(如Sum或Product)的位置
那么,GHCi如何推断正确的幺半群来使用呢?还是我在这一点上完全错了
示例来源:不需要
foldl
被翻译成foldr
,它被翻译成foldMap
而不是Endo
,这意味着函数组合,意味着对您提供的函数进行简单的嵌套
或者别的什么。也就是说,foldl
可以翻译成foldMap
overDual。Endo
由左到右等组成
更新:是的
可折叠实例应满足以下规律:
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
或等效值(此处为(+)
案例所示)应用于用户提供的值——在您的案例中
0
另一件需要注意的事情是Endo
和Dual
是newtype
s,因此所有这些机制都将由编译器完成,并在运行时消失。对于a->a
形式的普通函数,存在一个monoid实例(隐式,如果不是显式的话),其中,mappend
对应于函数组合,mempty
对应于id
函数
什么是(+)
?它是一个函数(Num a)=>a->a->a
。如果你用+
在你的可折叠的满是数字的地图上进行折叠,你会把每一个数字都变成部分应用的(+a
。瞧,你发现了神奇的f
,它会把你可折叠的地图上的所有东西都变成一个幺半群
假设函数有一个直接幺半群实例,您可以执行以下操作:
foldMap (+) [1, 2, 3, 4]
,这将生成一个最终的(Num a)=>a->a
,您可以将其应用于0
以获得10
但是,没有这样的直接实例,因此需要使用内置的newtype
包装器Endo
和相应的unwrapperappEndo
,它们为a->a
函数实现monoid
Prelude Data.Monoid> (appEndo (foldMap (Endo . (+)) [1, 2, 3, 4])) 0
10
这里的Endo.
只是我们烦人的需要,我们需要提升普通的a->a
s,这样它们就有了自然的Monoid
实例。在foldMap
完成后,通过将所有内容转换成a->a
s并用合成链接在一起,我们使用提取最终的a->a
ode>appEndo
,最后将其应用于0
简短回答:它是foldMap
签名中的类型约束
如果我们查看Foldable
(更具体地说是foldMap
)的源代码,我们会看到:
这意味着如果我们声明Tree
是Foldable
的一个成员(不是Tree
有kind*->*
),这意味着foldMap
是在树上定义的,这样:foldMap::monoidm=>(a->m)->treea->m
。因此这意味着结果类型(传递给foldMap
)的函数的结果必须是Monoid
Haskell是静态类型的:编译时之后,Haskell确切地知道每个函数实例中传递的类型。这意味着它知道输出类型是什么,以及如何处理它
现在Int
不是Monoid
的实例。您在这里使用F.foldl(+)0 testTree
,这意味着您或多或少构造了一个“特殊”Monoid。如果我们看一下:
这有很多围绕参数f
、z
和t
的逻辑。因此,让我们先将其分解
首先让我们看一下Dual.Endo.flip f
。它是以下的缩写:
helper = \x -> Dual (Endo (\y -> f y x))
Dual
和Endo
是每个构造函数都有一个参数的类型。因此,我们将fyx
的结果包装在Dual(Endo…
构造函数中
我们将使用它作为foldMap
的函数。如果我们的f
有类型a->b->a
,那么这个函数有类型b->Dual(Endo a)
。因此传递给foldMap
的函数的输出类型有输出类型Dual(Endo a)
。现在,如果我们检查源代码,我们会看到两件有趣的事情:
(请注意,它是y`mappend`x
,而不是x`mappend`y
)
因此,这里发生的是foldMap
中使用的mempty
是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))
这意味着如果我们折叠树,如果我们看到一个空的
(一片叶子),我们将返回mempty
,如果我们看到一个节点x l r
,我们将执行上面描述的映射
-- 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
因此,对于每个节点
我们在节点的子节点和项上从右到左进行函数组合。a
和c
也可以是树的组合(因为它们是递归调用)。对于叶
,我们什么也不做(我们返回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)
)
)
)
(省略了标识
mempty = Dual (Endo id)
mappend (Dual (Endo f)) (Dual (Endo g)) = Dual (Endo (g . f))
-- 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
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)
)
)
)
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)
)
( (\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)
)
f (f (f (f (f (f (f z 1) 3) 6) 5) 8) 9) 10