可折叠,Monoid和Monad

时间:2016-10-10 05:27:24

标签: haskell category-theory

考虑foldMap

的以下签名
foldMap :: (Foldable t, Monoid m) => (a -> m) -> t a -> m

这与“绑定”非常相似,只是交换了参数:

(>>=) :: Monad m => m a -> (a -> m b) -> m b

在我看来,FoldableMonoidMonad之间必然存在某种关系,但我无法在超类中找到它。据推测,我可以将其中的一个或两个转换为另一个,但我不确定如何。

这种关系可以详细说明吗?

3 个答案:

答案 0 :(得分:24)

MonoidMonad

哇,这实际上是我们可以使用引用的极少数时间之一:

  

monad只是endofunctors类别中的幺半群,[...]

让我们从一个幺半群开始。集合Set类别中的幺半群是一组元素m,其中包含空元素mempty和关联函数mappend以组合元素

mempty `mappend` x == x -- for any x
x `mappend` mempty == x -- for any x
-- and
a `mappend` (b `mappend` c) == (a `mappend` b) `mappend` c -- for all a, b, c

请注意,monoid不限于集合,类别(monad)类别Cat中也存在幺半群等。基本上任何时候你都有一个关联二进制操作和它的标识。

现在是monad,它是endofunctors类别中的" monoid"具有以下属性:

它是一个endofunctor,这意味着它在Haskell类型的类别* -> *中具有类型Hask

现在,为了更进一步,你必须知道我将在这里解释的一些类别理论:给定两个仿函数FG,存在F的自然变换到G如果存在函数α,则每个F a都可以映射到G aα可以是多对一的,但必须映射F a的每个元素。粗略地说,自然变换是算子之间的函数。

现在在类别理论中,两个类别之间可以存在许多仿函数。在简化的视图中,可以说我们甚至不关心哪些仿函数从何处映射到哪里,我们只关心它们之间的自然转换。

回到monad,我们现在可以看到endofunctors类别中的" monoid"必须进行两次自然变革。让我们调用我们的monad endofunctor M

从身份(endo)仿函数到monad的自然转换:

η :: 1 -> M -- this is return

从两个monad的位置自然转变并产生第三个:

μ :: M × M -> M

由于×是仿函数的组合,我们(粗略地说)也可以写:

μ :: m a × m a -> m a
μ :: (m × m) a -> m a
μ :: m (m a) -> m a -- join in Haskell

满足这些法律:

μ . M μ == μ . μ M
μ . M η == μ . η M

因此,monad是endofunctors类别中monoid的特例。你不能在正常的Haskell中为monad编写一个monoid实例,因为Haskell的组合概念太弱了(我认为;这是因为函数仅限于Hask而且它是#{1}}弱于Cat)。有关详细信息,请参阅this

Foldable怎么样?

现在和Foldable一样:fold的定义使用自定义二元函数来组合元素。现在你可以提供任何组合元素的功能,或者你可以使用现有的组合元素概念,即monoid。再次请注意,这个幺半群仅限于幺半群,幺半群的catorical定义。

由于幺半群mappend是关联的,foldlfoldr会产生相同的结果,这就是为什么幺半群的折叠可以减少到fold :: Monoid m, Foldable t => t m -> m 。这是monoid和foldable之间的明显联系。

@danidiaz已使用Applicative仿函数Monoid指出FoldableConstConst a b = Const a之间的关联,其应用实例需要第一个参数Const为幺半群(没有pure没有mempty(忽略undefined)。

比较monad和foldable在我看来有点拉伸,因为monad比可折叠更强大,因为foldable只能根据映射函数累积列表的值,但是monad绑定可以从结构上改变了背景(a -> m b)。

答案 1 :(得分:9)

摘要(>>=)traverse看起来相似,因为它们都是仿函数的箭头映射,而foldMap(几乎)是一个专门的traverse }。

在我们开始之前,有一些术语需要解释。考虑fmap

fmap :: Functor f => (a -> b) -> (f a -> f b)

Haskell Functor是一个来自 Hask 类别(带有Haskell函数的类别为箭头的类)的仿函数。在类别理论术语中,我们说(专门的)fmap是这个仿函数的箭头映射,因为它是将箭头带到箭头的仿函数的一部分。 (为了完整起见:仿函数由箭头映射和对象映射组成。在这种情况下,对象是Haskell类型,因此对象映射将类型转换为类型 - 更具体地说, Functor的对象映射是它的类型构造函数。)

我们还要记住类别和函子法则:

-- Category laws for Hask:
f . id = id
id . f = f
h . (g . f) = (h . g) . f

-- Functor laws for a Haskell Functor:
fmap id = id
fmap (g . f) = fmap g . fmap f

在下文中,我们将使用 Hask 以外的类别以及非Functor s的仿函数。在这种情况下,我们会使用适当的标识和组合替换id(.)fmap通过适当的箭头映射,在一种情况下,=取适当的等式箭头。

(= LT;≤)

从答案中更熟悉的部分开始,对于给定的monad ma -> m b函数(也称为Kleisli箭头)形成一个类别(m的Kleisli类别),return作为标识,(<=<)作为组合。在这种情况下,三类法律只是the monad laws

f <=< return = return
return <=< f = f
h <=<  (g <=<  f) = (h <=<  g) <=<  f

现在,您询问了翻转绑定:

(=<<) :: Monad m => (a -> m b) -> (m a -> m b)

事实证明,(=<<)是从m的Kleisli类别到 Hask 的仿函数的箭头映射。适用于(=<<)的仿函数法则相当于两个monad定律:

return =<< x = x -- right unit
(g <=< f) =<< x = g =<< (f =<< x) -- associativity 

横向

接下来,我们需要绕过Traversable(在答案的最后提供了本节结果证明的草图)。首先,我们注意到所有应用仿函数a -> f b的{​​{1}}函数一次采取(而不是每次一个,如指定Kleisli类别时)形成一个类别,f为身份,Identity为组合。为了实现这一点,我们还必须采用更宽松的箭头相等,忽略Compose . fmap g . fIdentity样板(这是必要的,因为我在伪Haskell中写这个,而不是适当的数学符号)。更准确地说,我们将考虑任何两个可以使用IdentityCompose同构的任意组合相互转换的函数作为相等的箭头(换句话说,我们不会区分{{1 }和Compose,以及aIdentity a之间。

让我们将该类别称为“可遍历类别”(因为我现在想不出更好的名字)。在具体的Haskell术语中,此类别中的箭头是一个函数,它在任何先前存在的层下面添加了f (g a)上下文“额外”层。现在,考虑Compose f g a

Applicative

给定可遍历容器的选择,traverse是仿函数从“可遍历类别”到其自身的箭头映射。它的仿函数法则适用于可遍及的法则。

简而言之,对于涉及 Hask 以外的类别的仿函数,traverse :: (Traversable t, Applicative f) => (a -> f b) -> (t a -> f (t b)) traverse都是(=<<)的类似物,因此它们的类型就不足为奇了有点相似。

foldMap

我们仍然需要解释所有这些与traverse有什么关系。答案是fmap可以从foldMap恢复(参见danidiaz's answer - 它使用foldMap,但由于应用仿函数为traverse,结果为基本上相同):

traverse_

感谢Const m / -- cf. Data.Traversable foldMapDefault :: (Traversable t, Monoid m) => (a -> m) -> (t a -> m) foldMapDefault f = getConst . traverse (Const . f) 同构,这显然等同于:

const

getConst专用于foldMapDefault' :: (Traversable t, Monoid m) => (a -> Const m b) -> (t a -> Const m (t b)) foldMapDefault' f = traverse f 应用仿函数的traverse。即使Monoid m => Const m不是TraversablefoldMapDefault不是Foldable,这也为foldMap的类型与foldMap的类型提供了合理的理由。和,traverse

的传递

作为最后一次观察,请注意,某些(=<<) Const m的“可遍历类别”的箭头与应用仿函数Monoid执行而非形成子类别,因为没有身份,除非m是应用函子的可能选择之一。这可能意味着从这个答案的角度来看,没有任何其他值得关注的Identity。给出子类别的唯一选择的applicative functor是foldMap,考虑到容器上Identity的遍历量等于Identity,这并不奇怪。

附录

这是fmap结果推导的粗略草图,从几个月前的笔记中抽出来,编辑很少。 traverse表示“等于(某些相关的)同构”。

~
-- Identity and composition for the "traversable category".
idT = Identity
g .*. f = Compose . fmap g . f

-- Category laws: right identity
f .*. idT ~ f
f .*. idT
Compose . fmap f . idT
Compose . fmap f . Identity
Compose . Identity . f
f -- using getIdentity . getCompose 

-- Category laws: left identity
idT .*. f ~ f
idT .*. f
Compose . fmap Identity . f
f -- using fmap getIdentity . getCompose

-- Category laws: associativity
h .*. (g .*. f) ~ (h .*. g) .*. f
h .*. (g .*. f) -- LHS
h .*. (Compose . fmap g . f)
Compose . fmap h . (Compose . fmap g . f)
Compose . Compose . fmap (fmap h) . fmap g . f
(h .*. g) .*. f -- RHS
(Compose . fmap h . g) .*. f
Compose . fmap (Compose . fmap h . g) . f
Compose . fmap (Compose . fmap h) . fmap g . f
Compose . fmap Compose . fmap (fmap h) . fmap g . f
-- using Compose . Compose . fmap getCompose . getCompose
Compose . Compose . fmap (fmap h) . fmap g . f -- RHS ~ LHS

答案 2 :(得分:7)

当容器为Foldable时,foldMapApplicativeMonad的超类)之间存在关联。

Foldable有一个名为traverse_的函数,其签名为:

traverse_ :: Applicative f => (a -> f b) -> t a -> f ()

一个可能的ApplicativeConstant。要成为一个应用者,它需要&#34;累加器&#34;参数为Monoid

newtype Constant a b = Constant { getConstant :: a } -- no b value at the term level!

Monoid a => Applicative (Constant a)

例如:

gchi> Constant (Sum 1) <*> Constant (Sum 2) :: Constant (Sum Int) whatever
Constant (Sum {getSum = 3})

我们可以用foldMaptraverse_这样定义Constant

foldMap' :: (Monoid m, Foldable t) => (a -> m) -> t a -> m
foldMap' f = getConstant . traverse_ (Constant . f)

我们使用traverse_浏览容器,使用Constant累积值,然后使用getConstant删除新类型。