monad只是内函子范畴中的一个monid,有什么问题吗?

下面是谁先说的?

单模只是单模中的一个内函子的类别,什么是有问题吗?

在不太重要的一点上,这是真的吗?如果是真的,你能给出一个解释吗(希望一个没有太多Haskell经验的人可以理解的解释)?

212312 次浏览

这个特别的短语是由詹姆斯·艾里,从他的高度娱乐编程语言的简短、不完整和大部分错误的历史中,他虚构地将其归因于菲利普·瓦德勒。

最初的引语来自桑德斯·麦克·莱恩的工作数学家的类别,这是范畴理论的基础文本之一。这是在上下文中,这可能是确切了解它的含义的最佳场所。

但是,我会试一试。原来的句子是这样的:

总而言之,X中的单子只是X的内函子范畴中的一个单半群,乘积×被内函子的组合和单位集所取代。

这里的X是一个类别。内函子是从一个类别到自身的函子(就函数式程序员而言,这通常是所有Functor,因为他们主要处理的只是一个类别;类型的类别——但我离题了)。但是你可以想象另一个类别,即“X上的内函子”类别。在这个类别中,对象是内函子,态射是自然变换。

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

一元是…

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

…满足这些法律:

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

单子是…

  • 一个内函子,T: X→X(在Haskell中,具有Functor实例的* -> *类型构造函数)
  • 自然转换,μ: T×T→T,其中×表示仿函数组合(μ在Haskell中称为#0
  • 一个自然变换,η: I→T,其中X上的恒等内函子(η在Haskell中被称为#0

…满足这些法律:

  • μTμ = μ ∘ μT
  • μTη = μ ∘ ηT=1(身份自然转换)

稍微斜视一下,您可能会看到这两个定义都是同一个抽象概念的实例。

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

{-# LANGUAGE RankNTypes, TypeOperators #-}
import Control.Monad (join)

其中,RankNTypes是唯一对下面的内容绝对重要的。我曾经写过一个关于#0的解释,有些人似乎觉得很有用,所以我将参考它。

引用汤姆·克罗克特的精彩回答,我们有:

单子是…

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

…满足这些法律:

  • μ(μ(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 :: [] :-> MaybelistToMaybe = Natural gowhere go [] = Nothinggo (x:_) = Just x
maybeToList :: Maybe :-> []maybeToList = Natural gowhere 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 wherefmap f (Identity a) = Identity (f a)

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

return' :: Monad t => Identity :-> treturn' = 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) wherefmap f (Compose fga) = Compose (fmap (fmap f) fga)

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

join' :: Monad t => Compose t t :-> tjoin' = 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 aQ.E.D.

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

下面是我的原始答案。


很有可能Iry读过从单子到单子。在这篇博文中,Dan PiPoni(sigfpe)从Haskell中的单半群中推导出了单子,其中有很多关于范畴理论的讨论,并明确提到了“哈斯克上的内函子范畴”。无论如何,任何想知道单子在内函子范畴中成为单半群意味着什么的人都可能会从阅读这篇推导中受益。

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

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

Mac Lane使用该描述来描述Monad的事实,有人可能暗示它描述了monad独有的东西。请原谅我。为了对该语句有更广泛的理解,我认为需要明确他是没有描述monad独有的东西;该语句同样描述了Application ative和Arrow等。出于同样的原因,我们可以在Int(Sum和Products)上有两个单参数,我们可以在X上有几个内函子类别的单参数。但相似性更多。

Monad和Application ative都符合以下条件:

  • endo=>在同一位置开始和结束的任何箭头或态射
  • 仿函数=>两个类别之间的任何箭头或态射

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

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

语句使用“…的类别”来定义语句的范围。例如,函数类别描述了f * -> g *的范围,即Any functor -> Any functor,例如Tree * -> List *Tree * -> Tree *

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

在这种情况下,在函子内部,没有指定* -> *,也就是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+仿函数:: f a -> f b(即,任何一个参数化类型到相同的参数化类型)…
  • 内函子范畴中的一元半群

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

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

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

  • 在外部,它显示为单个对象(例如,:: List);静态
  • 但在里面,允许很多动态
    • 任何数量的相同类型的值(例如,空|~Non空)作为任何程度函数的素材。张量积将把任何数量的输入减少到一个值…对于外部层(~fold没有说明有效载荷)
    • 两者的无限范围最内层的类型和值

在Haskell中,澄清语句的适用性很重要。这个构造的强大性和多功能性与monad本身完全无关。换句话说,构造不依赖于monad的独特性。

当试图弄清楚是否应该构建具有共享上下文的代码来支持相互依赖的计算,而不是可以并行运行的计算时,这个臭名昭著的声明,尽管它描述了,但它不是应用程序、箭头和单子选择之间的对比,而是对它们在多大程度上相同的描述。对于手头的决定,这个声明没有意义。

这经常被误解。该陈述继续将join :: m (m a) -> m a描述为一元内函子的张量积。然而,它没有阐明在该陈述的上下文中,(<*>)也可以被选择。它确实是一个“六合一,半打合一”的例子。组合值的逻辑完全相同;相同的输入产生相同的输出(与Int的Sum和乘积单元格不同,因为它们在组合Ints时产生不同的结果)。

所以,总结一下:内函子范畴中的一元半群描述了:

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

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

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

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

然而,具有讽刺意味的是,我最初是如何误解了这句话在monad之外的适用性的,以及我希望在这里传达的内容。它描述的一切最终都与Application ative和Monad(以及Arrow等)之间的相似之处。它没有说的正是它们之间微小但有用的区别。

这里的答案在定义整数和单子方面做得很好,然而,他们似乎仍然没有回答这个问题:

在不太重要的一点上,这是真的吗?如果是真的,你能给出一个解释吗(希望一个没有太多Haskell经验的人可以理解的解释)?

这里缺少的问题的关键,是“一元”的不同概念,更准确地说,所谓的分类——一元范畴中的一元。可悲的是,Mac Lane的书本身让人很困惑

总而言之,X中的一个单子只是X的内函子范畴中的一个单半群,产品×被内函子的组合和单位集所取代。

主要混淆

为什么这会令人困惑?因为它没有定义什么是X的“内函子范畴中的monid”。相反,这句话建议将所有内函子的集合中的monid与仿函数组合一起作为二进制操作,将恒等仿函数作为一元单元。它工作得非常好,并将包含恒等仿函数并在仿函数组合下封闭的任何内函子集变成monid。

然而,这不是正确的解释,这本书在那个阶段没有弄清楚。Monadf是一个固定的内函子,而不是在组合下封闭的内函子的子集。一个常见的构造是使用ff0的一元半群,方法是将ff^k = f(f(...))的所有k折叠组合的集合与自身一起使用,包括与恒等6相对应的k=0。现在,所有k>=0的所有这些幂的集合S确实是一个单半群,“乘积×被内函子的组合和单位由恒等内函子所取代”。

然而:

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

为了让事情更混乱,“一元范畴中的一元态”的定义在本书后面出现,你可以从目录中看到。然而,理解这个概念对于理解与单子的联系绝对至关重要。

(严格)一元范畴

转到第七章(比第六章晚),我们发现所谓的严格一元范畴定义为三元(B, *, e),其中B是一个类别,*: B x B-> B双函子(每个组件的仿函数与其他组件固定),eB中的单位对象,满足结合性和单位定律:

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

对于B中的任何对象a,b,c,以及eid_e替换的任何态射a,b,c的相同恒等式,e的恒等式态射。现在观察到,在我们感兴趣的情况下,其中BX的内函子范畴,自然变换为态射,*是仿函数组合,e是恒等式仿函数,所有这些定律都满足,可以直接验证。

书中接下来的是“松弛”一元范畴的定义,其中定律仅包含满足所谓连贯关系的一些固定的自然变换,但是对于我们的内函子范畴的情况并不重要。

一元范畴中的一元类

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

一元类(B, *, e)中的一元类cB的对象,具有两个箭头(态射)

mu: c * c -> cnu: e -> c

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

最后,请注意,3个交换图(在一元范畴中的一元半群的定义中)是针对一般(非严格)一元范畴编写的,而在我们的例子中,作为一元范畴的一部分产生的所有自然变换实际上都是恒等式。这将使图与单子定义中的图完全相同,使对应完整。

结论

总之,根据定义,任何monad都是内函子,因此是内函子范畴中的对象,其中monadicjoinreturn运算符满足那个特殊(严格)一元范畴中的一元的定义。反之亦然,根据定义,内函子一元范畴中的任何monid是由一个对象和两个箭头组成的三重(c, mu, nu),例如我们的情况下的自然变换,满足与monad相同的定律。

最后,请注意(经典的)一元类和一元类中更一般的一元类之间的关键区别。上面的两个箭头munu不再是二进制操作和集合中的单元。相反,你有一个固定的内函子c。尽管书中有一句令人困惑的话,但仿函数组合*和单位仿函数本身并不能提供单子所需的完整结构。

另一种方法是与集合A的所有自映射的标准单体C进行比较,其中二进制运算是组合,可以看出将标准笛卡尔积C x C映射到C。传递到分类的单体,我们将笛卡尔积x替换为仿函数组合*,并且二进制运算被替换为来自c * cc,这是join运算符的集合

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

对于每个对象T(编程中的类型)。并且可以用来自固定单点集的映射图像来识别的经典一元组中的标识元素被return运算符的集合替换

return: T->c(T)

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