例如,在Haskell的distributive
library docs中提到了共生体:
由于Haskell中缺少非平凡的共生,我们可以限制自己需要一个Functor而不是一些Coapplicative类。
经过一番搜索后,我找到了一个StackOverflow answer,它更多地解释了共生必须满足的规律。所以我想我理解为什么Haskell中假设的Comonoid类型类只有一个可能的实例。
因此,为了找到一个非常重要的共生体,我想我们必须寻找其他类别。当然,如果类别理论家有共同体的名称,那么有一些有趣的。该页面上的其他答案似乎暗示了一个涉及Supply
的例子,但我无法弄清楚仍然符合法律的一个例子。
我也转向维基百科:有一个没有引用类别理论的幺半群的页面,在我看来这是对Haskell的Monoid
类型类的充分描述,但是& #34; comonoid"重定向到我无法理解的幺半群和共生类别的类别理论描述,但似乎还没有任何有趣的例子。
所以我的问题是:
编辑:我不确定这实际上是否在类别理论上是正确的,但我在问题2的括号中想象的是delete :: a -> m ()
和{{1}的非平凡定义对于某些特定的 Haskell类型split :: a -> m (a, a)
和Haskell monad a
,它们满足链接答案中comonoid定律的Kleisli-arrow版本。其他的comonoids例子仍然受欢迎。
答案 0 :(得分:42)
正如Phillip JF所提到的,comonoids在子结构逻辑中很有趣。我们来谈谈线性lambda演算。这非常类似于普通类型的lambda演算,除了每个变量必须只使用一次。
要感受一下,请count linear functions of given types,即
a -> a
只有一个居民,id
。而
(a,a) -> (a,a)
有两个,id
和flip
。请注意,在常规lambda演算(a,a) -> (a,a)
中有四个居民
(a, b) ↦ (a, a)
(a, b) ↦ (b, b)
(a, b) ↦ (a, b)
(a, b) ↦ (b, a)
但前两个要求我们使用其中一个参数而丢弃另一个。这正是线性lambda演算的本质 - 不允许这些函数。
快速地说,线性LC的重点是什么?好吧,我们可以用它来模拟线性效应或资源使用。例如,如果我们有一个文件类型和一些变换器,它可能看起来像
data File
open :: String -> File
close :: File -> () -- consumes a file, but we're ignoring purity right now
t1 :: File -> File
t2 :: File -> File
然后以下是有效的管道:
close . t1 . t2 . open
close . t2 . t1 . open
close . t1 . open
close . t2 . open
但是这个"分支"计算不是
let f1 = open "foo"
f2 = t1 f1
f3 = t2 f1
in close f3
因为我们两次使用f1
。
现在,您可能想知道关于线性规则必须遵循的内容。例如,我决定某些管道不必同时包含t1
和 t2
(比较之前的枚举练习)。此外,我介绍了open
和close
函数,这些函数可以快乐地创建和销毁File
类型,尽管它违反了线性。
实际上,我们可能会认为存在违反线性的函数 - 但并非所有客户都可能。它与IO
monad很相似 - 所有秘密都存在于IO
的实现中,因此用户可以使用纯粹的"世界。
这就是Comonoid
的用武之地。
class Comonoid m where
destroy :: m -> ()
split :: m -> (m, m)
在线性lambda演算中实例化Comonoid
的类型是具有随身破坏和复制规则的类型。换句话说,它根本不受线性lambda演算的约束。
由于Haskell根本没有实现线性lambda演算规则,我们总是可以实例化Comonoid
instance Comonoid a where
destroy a = ()
split a = (a, a)
或许,另一种思考方式是,Haskell是一个线性LC系统,恰好可以为每种类型实例Comonoid
并自动应用destroy
和split
答案 1 :(得分:17)
答案 2 :(得分:1)
我们可以想到一个monoid的方式就像我们正在使用的任何特定产品结构一样,所以在Set中我们会采用这个签名:
mul : A * A -> A
one : A
到这一个:
dup : A -> A * A
one : A
但是二元性的概念是你可以制作的逻辑语句都具有可以应用于双重对象的双重语句,并且还有另一种方式来说明幺半群是什么,并且这与产品的选择无关。构造然后当我们采用结构时,我们可以在输出中获取副产品,例如:
div : A -> A + A
one : A
其中+是标记的总和。在这里,我们基本上已经知道,这种类型中的每个单词都随时可以生成一个新位,这个位隐含地来自用于表示A的左侧或右侧实例的标记。认为这真是太酷了。我认为人们在上面讨论的事情的酷版本是当你没有特别为幺半群构建时,而是为了幺半群行动。
如果有一个函数
,则称一个幺半群M作用于集合A.act : M * A -> A
我们有以下规则
act identity a = a
act f (act g a) = act (f * g) a
如果我们想要合作,我们到底想要什么?
act : A -> M * A
这会为我们生成一个类型的comonoid流!我在为这些系统制定法律方面遇到了很多麻烦,但我认为它们必须在某个地方,所以我今晚要继续寻找。如果有人可以告诉我他们或者我以某种方式对这些事情做错了,也对此感兴趣。
答案 3 :(得分:0)
作为物理学家,我所处理的最常见的例子是余子代数,即向量空间范畴中的彗形对象,其单曲面结构通常由张量积给出。
在这种情况下,由于可以仅对乘积和单位图进行伴随或转置,从而得到满足comonoid公理的副产品和共单位,因此,在单面体和共形对象之间存在双射。
在物理学的某些分支中,常见的情况是看到具有代数和联结结构且具有一些相容性公理的对象。两种最常见的情况是Hopf代数和Frobenius代数。它们对于构造纠缠或相关的状态或解非常方便。
在编程中,我能想到的最简单的简单例子是引用计数的指针,例如C ++中的shared_ptr和Rust中的Rc以及它们的弱等效项。您可以复制它们,这是一项繁琐的操作,会增加引用计数(因此这两个副本与初始状态不同)。您可以放一个(称为析构函数),这是不平凡的,因为它会降低指向相同数据的任何其他经过引用的指针的引用计数。
此外,弱指针是comonoid 动作的一个很好的例子。您可以使用协作从共享指针生成弱指针。通过注意到从共享指针创建一个并立即删除它是一个单元操作,然后创建一个并克隆它等同于从共享指针创建两个,可以很容易地检查到这一点。
这是非平凡的副产品及其协同作用的普遍现象:当它们不简化为复制操作时,它们直观地暗示了在两个半部之间的距离处有某种形式的操作,同时还添加了一个操作消除了一半,而使另一半独立。