【问题标题】:A monad is just a monoid in the category of endofunctors, what's the problem?单子只是内函子类别中的一个幺半群,有什么问题?
【发布时间】:2011-04-21 16:04:17
【问题描述】:

以下是谁先说的?

单子只是一个幺半群 内函子的类别,什么是 有问题吗?

还有一个不太重要的说明,这是真的吗?如果是的话,您能否给出解释(希望没有太多 Haskell 经验的人可以理解)?

【问题讨论】:

  • 参见“工作数学家的分类”
  • 在 Haskell 中使用 monad 不需要了解这一点。从实用的角度来看,它们只是通过一些地下管道传递“状态”的巧妙方法。
  • 我也想在这里添加这篇优秀的博客文章:stephendiehl.com/posts/monads.html 它并没有直接回答这个问题,但在我看来,斯蒂芬在将 Haskell 中的类别和单子联系在一起方面做得非常出色.如果您已阅读上述答案 - 这应该有助于统一看待这个问题的两种方式。
  • 更准确地说“对于任何类别 C,其内函子的类别 [C,C] 具有由组合诱导的幺半群结构。[C,C] 中的幺半群对象是 C 上的单子。 " - 来自 en.wikipedia.org/wiki/Monoid_%28category_theory%29。有关范畴论中 monad 的定义,请参见 en.wikipedia.org/wiki/Monad_%28category_theory%29。
  • @Dmitry functor 是一个类别之间的函数,需要遵守一些约束条件。类别 C 上的内函子只是从 C 到自身的函子。 Data.FunctorHask category 上的 endofunctors 的类型类。由于类别由对象和态射组成,函子需要映射两者。对于 Data.Functor 的实例 f,对象(haskell 类型)上的映射是 f 本身,而态射(haskell 函数)上的映射是 fmap。

标签: haskell monads category-theory monoids


【解决方案1】:

这个特别的措辞来自 James Iry,来自他非常有趣的 Brief, Incomplete and Mostly Wrong History of Programming Languages,他在其中虚构地将其归因于 Philip Wadler。

原引自 Saunders Mac Lane 在工作数学家的类别中,类别理论的基础文本之一。 Here it is in context,这可能是了解其含义的最佳地点。

但是,我会采取行动。原句是这样的:

总而言之,X 中的 monad 只是 X 的内函子范畴中的一个幺半群,乘积 × 被内函子的组合替换,单位由恒等函子设置。

X 这里是一个类别。 Endofunctors 是从一个类别到自身的函子(就函数式程序员而言,这通常是 all Functors,因为他们主要处理一个类别;类型的类别 - 但我离题)。但是您可以想象另一个类别,即“X 上的内函子”类别。这是一个对象是内函子,态射是自然变换的范畴。

在这些内函子中,其中一些可能是单子。哪些是单子?正是在特定意义上是 monoidal 的那些。与其详细说明从 monad 到 monoid 的确切映射(因为 Mac Lane 做得比我希望的要好得多),我只是将它们各自的定义并排放置并让您比较:

幺半群是...

  • 一组,S
  • 一个操作,• : S × S → S
  • S的一个元素,e : 1 → S

...满足这些法律:

  • (a • b) • c = a • (b • c),对于所有 abcS
  • e • a = a • e = a,对于 S 中的所有 a

单子是...

  • 内函子,T : X → X(在 Haskell 中,类型构造函数 * -> * 带有 Functor 实例)
  • 自然变换,μ : T × T → T,其中 × 表示函子组合(μ在 Haskell 中称为 join
  • 自然变换,η : I → T,其中 IX 上的恒等函数( η 在 Haskell 中称为 return

...满足这些法律:

  • μ ∘ Tμ = μ ∘ μT
  • μ ∘ Tη = μ ∘ ηT = 1(恒等式自然变换)

稍稍眯起眼,您可能会发现这两个定义都是同一个abstract concept 的实例。

【讨论】:

  • 感谢您的解释,并感谢编程语言的简要、不完整和大部分错误的历史一文。我想它可能来自那里。确实是最伟大的编程幽默之一。
  • @Jonathan:在幺半群的经典表述中,× 表示集合的笛卡尔积。您可以在此处阅读更多相关信息:en.wikipedia.org/wiki/Cartesian_product,但基本思想是 S × T 的元素是一对 (s, t),其中 s ∈ St ∈ T。所以单曲面积 • : S × S -> S 在这种情况下的签名仅仅意味着一个函数,它将 S 的 2 个元素作为输入并产生 S 的另一个元素i>S 作为输出。
  • @TahirHassan - 在范畴论的一般性中,我们处理不透明的“对象”而不是集合,因此没有“元素”的先验概念。但是,如果您考虑对象是集合而箭头是函数的类别 Set,那么任何集合 S 的元素都与任何单元素集合中的函数一一对应S。即对于S的任何元素e,只有一个函数f : 1 -> S,其中1 是任何单元素集合...(续)
  • @TahirHassan 一元集本身是“终端对象”更一般的范畴理论概念的特化:终端对象是一个范畴的任何对象,对于该范畴,任何其他对象都只有一个箭头反对它(您可以检查 Set 中的 1 元素集是否为真)。在范畴论中,终端对象简称为1;它们在同构中是唯一的,因此没有必要区分它们。所以现在我们对任何 S 的“S 的元素”有一个纯粹的类别理论描述:它们只是从 1 的箭头b>S
  • @TahirHassan - 用 Haskell 术语来说,想想如果S 是一种类型,那么在编写函数f :: () -> S 时你所能做的就是挑选一些特定的类型术语S(它的一个“元素”,如果你愿意的话)并返回它......你没有得到参数的真实信息,所以没有办法改变函数的行为。所以f 必须是一个常量函数,每次都返回相同的东西。 () ("Unit") 是 Hask 类别的终端对象,其中恰好有 1 个(非发散的)值。
【解决方案2】:

首先,我们将要使用的扩展和库:

{-# LANGUAGE RankNTypes, TypeOperators #-}

import Control.Monad (join)

其中,RankNTypes 是唯一对以下内容绝对必要的。 I once wrote an explanation of RankNTypes that some people seem to have found useful,所以我会参考。

引用Tom Crockett's excellent answer,我们有:

单子是...

  • 内函子,T : X -> X
  • 自然变换,μ : T × T -> T,其中 × 表示函子组合
  • 自然变换,η : I -> T,其中 IX 上的恒等函子

...满足这些法律:

  • μ(μ(T × T) × T)) = μ(T × μ(T × T))
  • μ(η(T)) = T = μ(T(η))

我们如何将其转换为 Haskell 代码?好吧,让我们从自然变换的概念开始:

-- | A natural transformations between two 'Functor' instances.  Law:
--
-- > fmap f . eta g == eta g . fmap f
--
-- Neat fact: the type system actually guarantees this law.
--
newtype f :-> g =
    Natural { eta :: forall x. f x -> g x }

f :-> g 形式的类型类似于函数类型,但不是将其视为两个类型之间的函数(类型为* ),将其视为两个 函子(每个类型 * -> *)之间的 态射。例子:

listToMaybe :: [] :-> Maybe
listToMaybe = Natural go
    where go [] = Nothing
          go (x:_) = Just x

maybeToList :: Maybe :-> []
maybeToList = Natural go
    where go Nothing = []
          go (Just x) = [x]

reverse' :: [] :-> []
reverse' = Natural reverse

基本上,在 Haskell 中,自然转换是从某种类型 f x 到另一种类型 g x 的函数,这样 x 类型变量对于调用者来说是“不可访问的”。例如,sort :: Ord a => [a] -> [a] 不能进行自然转换,因为它对于我们可以为a 实例化哪些类型是“挑剔的”。我经常用一种直观的方式来思考这个问题:

  • 仿函数是一种在不触及结构的情况下对某事物的内容进行操作的方式。
  • 自然转换是一种在不接触或查看内容的情况下对某事物的结构进行操作的方式。

现在,让我们处理定义的子句。

第一个子句是“一个内函子,T : X -> X。”好吧,Haskell 中的每个Functor 都是人们称之为“Hask 范畴”的内函子,其对象是 Haskell 类型(类型为 *),其态射是 Haskell 函数。这听起来像是一个复杂的陈述,但它实际上是一个非常微不足道的陈述。这意味着Functor f :: * -> * 为您提供了为任何a :: * 构造类型f a :: * 和从任何f :: a -> b 中构造函数fmap f :: f a -> f b 的方法,并且这些遵循函子定律。

第二个子句:Haskell 中的Identity 函子(平台自带,所以你可以直接导入)是这样定义的:

newtype Identity a = Identity { runIdentity :: a }

instance Functor Identity where
    fmap f (Identity a) = Identity (f a)

因此,对于任何Monad 实例t,Tom Crockett 定义的自然变换 η : I -> T 可以这样写:

return' :: Monad t => Identity :-> t
return' = Natural (return . runIdentity)

第三条:Haskell 中两个函子的组合可以这样定义(平台也自带):

newtype Compose f g a = Compose { getCompose :: f (g a) }

-- | The composition of two 'Functor's is also a 'Functor'.
instance (Functor f, Functor g) => Functor (Compose f g) where
    fmap f (Compose fga) = Compose (fmap (fmap f) fga)

因此,Tom Crockett 定义的自然变换 μ : T × T -> T 可以写成这样:

join' :: Monad t => Compose t t :-> t
join' = Natural (join . getCompose)

声明这是一个内函子类别中的幺半群然后意味着Compose(部分应用于它的前两个参数)是关联的,而Identity是它的标识元素。即,以下同构成立:

  • Compose f (Compose g h) ~= Compose (Compose f g) h
  • Compose f Identity ~= f
  • Compose Identity g ~= g

这些很容易证明,因为 ComposeIdentity 都定义为 newtype,并且 Haskell 报告将 newtype 的语义定义为定义的类型和参数类型之间的同构到newtype 的数据构造函数。所以举个例子,让我们证明Compose f Identity ~= f

Compose f Identity a
    ~= f (Identity a)                 -- newtype Compose f g a = Compose (f (g a))
    ~= f a                            -- newtype Identity a = Identity a
Q.E.D.

【讨论】:

  • Natural newtype 中,我无法弄清楚(Functor f, Functor g) 约束在做什么。你能解释一下吗?
  • @dfeuer 它并没有真正做任何必要的事情。
  • @LuisCasillas 我已经删除了那些 Functor 约束,因为它们似乎没有必要。如果您不同意,请随时将它们添加回来。
  • 您能否详细说明一下将函子的乘积视为组合的正式含义是什么?特别是,函子组合的投影态射是什么?我的猜测是,该乘积仅针对自身的函子 F 定义,F x F 并且仅在定义 join 时。而join 就是投影态射。但我不确定。
  • 我不认为最后列出的三个同构与说“内函子类别中的monoid”完全一样。特别是他们根本没有提到join'return'
【解决方案3】:

这里的答案在定义幺半群和单子方面做得很好,但是,他们似乎仍然没有回答这个问题:

还有一个不太重要的说明,这是真的吗?如果是的话,您能否给出解释(希望没有太多 Haskell 经验的人可以理解)?

这里缺少的问题的症结在于“monoid”的不同概念,更准确地说是所谓的分类 - monoidal 类别中的幺半群。可悲的是 Mac Lane 的书本身makes it very confusing

总而言之,X 中的 monad 只是 X endofunctors 类别中的一个幺半群,产品 × 被 endofunctors 的组合替换,单位由身份 endofunctor 设置。

主要混淆

为什么会令人困惑?因为它没有定义X 的“内函数类中的monoid”是什么。相反,这句话建议在所有内函子的集合中取一个幺半群,将函子组合作为二元运算,将恒等函子作为一个幺半群单元。它工作得非常好,并变成一个幺半群,包含恒等函子并在函子组合下封闭的内函子的任何子集。

然而,这不是正确的解释,这本书在那个阶段没有明确说明。 Monad f 是一个固定 endofunctor,而不是在组合下封闭的endofunctor 的子集。一种常见的构造是使用f生成一个幺半群,方法是使用f 的所有f^k = f(f(...)) 的集合,包括对应于身份f^0 = id。现在,所有k>=0 的所有这些权力的集合S 确实是一个幺半群,“其中乘积×被内函子的组合取代,单位由恒等函子设置”。

然而:

  • 这个幺半群S 可以定义为任何函子f,甚至可以定义为X 的任何自映射。是f生成的幺半群。
  • 由函子组合和恒等函子给出的 S 的幺半群结构与 f 是否是单子无关。

为了让事情更混乱,“幺半群中的幺半群”的定义出现在本书后面,您可以从table of contents 中看到。然而,理解这个概念对于理解与 monad 的联系是绝对关键的。

(严格)幺半群类别

转到关于 Monoids 的第 VII 章(晚于关于 Monads 的第 VI 章),我们发现所谓的严格 monoidal 类别的定义为三元组(B, *, e),其中B 是一个类别,*: B x B-> B一个bifunctor(对于每个组件的函子,其他组件固定),eB中的一个单位对象,满足关联性和单位定律:

(a * b) * c = a * (b * c)
a * e = e * a = a

对于B 的任何对象a,b,c,以及e 替换为id_e 的任何态射a,b,c 的相同恒等式,e 的恒等态射。现在观察到,在我们感兴趣的情况下,B 是具有自然变换作为态射的 X 的内函子类别,* 函子组合和 e 恒等函子,所有这些定律都是满意,可以直接验证。

本书后面是“松弛”monoidal category的定义,其中定律只对满足所谓相干关系的一些固定自然变换进行模数运算 ,但这对于我们的内函子类别并不重要。

幺半群类别中的幺半群

最后,在第七章的第 3 节“Monoids”中给出了实际的定义:

幺半群c 在幺半群类别(B, *, e)B 的对象,带有两个箭头(态射)

mu: c * c -> c
nu: e -> c

使 3 个图可交换。回想一下,在我们的例子中,这些是内函子范畴中的态射,它们是精确对应于单子的joinreturn 的自然变换。当我们使组合 * 更加明确时,这种联系变得更加清晰,将 c * c 替换为 c^2,其中 c 是我们的 monad。

最后,请注意 3 个交换图(在幺半群中的幺半群的定义中)是为一般(非严格)幺半群类别编写的,而在我们的例子中,作为幺半群的一部分产生的所有自然变换实际上都是身份。这将使图表与 monad 定义中的图表完全相同,从而使对应关系完整。

结论

总之,根据定义,任何 monad 都是 endofunctor,因此是 endofunctor 类别中的一个对象,其中 monadic joinreturn 运算符满足该特定(严格)monoidal 中的幺半群的定义类别。反之亦然,根据定义,内函子类中的任何幺半群都是由一个对象和两个箭头组成的三元组(c, mu, nu),例如在我们的例子中是自然变换,满足与单子相同的规律。

最后,请注意(经典)幺半群与幺半群类别中更一般的幺半群之间的主要区别。上面的两个箭头munu 不再是二元运算和集合中的一个单元。相反,您有一个固定的内函子c。函子组合 * 和恒等函子本身并不能提供 monad 所需的完整结构,尽管书中有令人困惑的评论。

另一种方法是与集合A 的所有自映射的标准幺半群C 进行比较,其中二元运算是组合,可以看出将标准笛卡尔积C x C 映射到C。传递给分类的幺半群,我们将笛卡尔积 x 替换为函子组合 *,二元运算被替换为自然变换 mu 来自 c * cc,这是 join 运算符的集合

join: c(c(T))->c(T)

对于每个对象T(输入编程)。而经典幺半群中的恒等元素,可以用来自固定单点集的地图图像来识别,被return 运算符的集合替换

return: T->c(T) 

但是现在没有更多的笛卡尔积,所以没有元素对,因此也没有二元运算。

【讨论】:

  • 那么您对问题的“这是真的”部分的回答是什么?单子是内函子类别中的幺半群,对吗?如果是的话,幺半群的范畴论概念和代数幺半群(具有关联乘法和单位的集合)之间的关系是什么?
  • @AlexanderBelopolsky,从技术上讲,monad 是内函子中的幺半群,它配备函子组合作为其产品。相反,经典的“代数幺半群”是在以笛卡尔积作为其乘积的集合的幺半群类别中的幺半群。因此,两者都是 monoid 的相同一般分类定义的特定情况。
  • @AlexanderBelopolsky 我试图解释答案是“是”,但是当在固定类别的内函子类别中采用更技术性的分类概念时。更容易更熟悉的“代数幺半群”是集合类别中的(分类)幺半群。正如上面 K.A.Buhr 所说,您可以将它们视为相同一般结构的不同特例。
【解决方案4】:

我来到这篇文章是为了更好地理解 Mac Lane 的工作数学家的分类理论中臭名昭著的引用的推断。

在描述某物是什么时,描述它不是什么通常同样有用。

Mac Lane 使用描述来描述 Monad 的事实可能暗示它描述了 monad 独有的东西。忍受我。为了更广泛地理解该陈述,我认为需要明确他不是在描述单子独有的东西;该声明同样描述了 Applicative 和 Arrows 等。出于同样的原因,我们可以在 Int 上拥有两个幺半群(Sum 和 Product),我们可以在 X 上拥有几个属于内函子类别的幺半群。但还有更多相似之处。

Monad 和 Applicative 都符合条件:

  • endo => 任何箭头,或在同一个地方开始和结束的态射
  • functor => 任意箭头,或两个类别之间的态射

    (例如,在日常Tree a -> List b 中,但在类别Tree -> List 中)

  • monoid => 单个对象;即,单一类型,但在这种情况下,仅与外部层有关;所以,我们不能有Tree -> List,只有List -> List

语句使用“...的类别”这定义了语句的范围。例如,Functor Category 描述了f * -> g * 的范围,即Any functor -> Any functor,例如Tree * -> List *Tree * -> Tree *

分类语句未指定的内容描述了任何事物都被允许的位置

在这种情况下,在函子内部,* -> * aka a -> b 未指定,这意味着 Anything -> Anything including Anything else。当我的想象力跳到 Int -> String 时,它还包括Integer -> Maybe Int,甚至Maybe Double -> Either String Int,其中a :: Maybe Double; b :: Either String Int

所以声明如下:

  • 函子作用域:: f a -> g b(即任何参数化类型到任何参数化类型)
  • endo + functor :: f a -> f b(即,任何一种参数化类型到相同的参数化类型)...不同的说法,
  • 内函子类别中的幺半群

那么,这个构造的力量在哪里?为了欣赏完整的动态,我需要看到一个幺半群的典型绘图(单个对象,看起来像一个标识箭头,:: single object -> single object),无法说明我被允许使用用 参数化的箭头任意数量的幺半群值,来自 Monoid 中允许的 one 类型对象。 endo, ~ 等价的标识箭头定义忽略函子的类型值以及最内层“有效负载”层的类型和值。因此,等价在任何函数类型匹配的情况下都会返回true(例如,Nothing -> Just * -> Nothing 等价于Just * -> Just * -> Just *,因为它们都是Maybe -> Maybe -> Maybe)。

侧边栏:~ 外部是概念性的,但在f a 中是最左边的符号。它还描述了“Haskell”首先读入的内容(大图);所以类型相对于类型值是“外部”的。编程中层之间的关系(引用链)在 Category 中不容易关联。 Set 的类别用于描述类型(Int、Strings、Maybe Int 等),其中包括 Functor 的类别(参数化类型)。引用链:Functor 类型、Functor 值(该 Functor 集合的元素,例如 Nothing、Just),以及每个 functor 值指向的其他所有内容。在 Category 中,对关系的描述不同,例如,return :: a -> m a 被认为是从一个 Functor 到另一个 Functor 的自然转换,与迄今为止提到的任何内容都不同。

回到主线,总而言之,对于任何定义的张量积和中性值,该语句最终描述了一个从其矛盾结构中诞生的惊人强大的计算结构:

  • 在外部显示为单个对象(例如,:: List);静态的
  • 但在内部,允许大量动态
    • 任何数量的相同类型的值(例如,Empty | ~NonEmpty)作为任何数量的函数的素材。张量积会将任意数量的输入减少为单个值...对于外部层(~fold 没有说明有效负载)
    • 无限范围最内层的类型和值

在 Haskell 中,阐明语句的适用性很重要。这种构造的强大功能和多功能性与单子本身完全无关。换句话说,该构造不依赖于使 monad 独一无二的原因。

当试图弄清楚是否构建具有共享上下文的代码以支持相互依赖的计算,而不是可以并行运行的计算时,这个臭名昭著的陈述,尽管它所描述的那样多,但这并不是两者之间的对比Applicative、Arrows 和 Monads 的选择,而是描述它们有多少相同。对于手头的决定,该声明没有实际意义。

这经常被误解。该声明继续将join :: m (m a) -> m a 描述为单曲面内函子的张量积。但是,它没有说明如何在本声明的上下文中也选择(<*>)。这确实是六/六打的一个例子。组合值的逻辑完全相同;相同的输入从每个生成相同的输出(与 Int 的 Sum 和 Product 幺半群不同,因为它们在组合 Int 时会生成不同的结果)。

所以,回顾一下:endofunctors 类别中的一个幺半群描述:

   ~t :: m * -> m * -> m *
   and a neutral value for m *

(<*>)(>>=) 都提供对两个 m 值的同时访问,以便计算单个返回值。用于计算返回值的逻辑完全相同。如果不是因为它们参数化的函数的不同形状(f :: a -> bk :: a -> m b)以及具有相同计算返回类型的参数的位置(即,a -> b -> bb -> a -> b 分别用于每个) ,我怀疑我们可以参数化幺半群逻辑,张量积,以便在两个定义中重用。作为说明这一点的练习,尝试实现~t,最终得到(<*>)(>>=),具体取决于您决定如何定义它forall a b

如果我的最后一点在概念上至少是正确的,那么它解释了 Applicative 和 Monad 之间精确且唯一的计算差异:它们参数化的函数。换句话说,区别在于这些类型类的实现外部

总之,根据我自己的经验,Mac Lane 臭名昭著的名言提供了一个很棒的“goto”模因,这是我在浏览 Category 以更好地理解 Haskell 中使用的习语时参考的指南。它成功地捕捉到了在 Haskell 中非常容易访问的强大计算能力的范围。

但是,具有讽刺意味的是,我最初是如何误解了该声明在 monad 之外的适用性,以及我希望在这里传达的内容。它所描述的一切都证明是 Applicative 和 Monads(以及 Arrows 等)之间的相似之处。它没有说的正是它们之间微小但有用的区别。

-E

【讨论】:

    【解决方案5】:

    注意: 不,这不是真的。在某些时候,丹·皮波尼本人对这个答案发表了评论,说这里的因果关系完全相反,他写这篇文章是为了回应詹姆斯·艾瑞的俏皮话。但它似乎已经被移除了,也许是被一些强迫性的整理者移除了。

    以下是我的原始答案。


    Iry 很可能读过 From Monoids to Monads,一篇文章 Dan Piponi (sigfpe) 从 Haskell 中的幺半群派生单子,其中大量讨论了范畴论并明确提到了“Hask 上的 endofunctors 范畴” .无论如何,任何想知道单子成为内函子类别中的幺半群意味着什么的人都可能从阅读这个推导中受益。

    【讨论】:

    • “也许是出于某种强迫性的整洁”——或者,正如我们在本网站上所称的那样,版主 :-)
    猜你喜欢
    • 2018-10-05
    • 1970-01-01
    • 1970-01-01
    • 2014-06-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多