一个单子是否可以成为一个余单子?

29

我知道什么是单子。我认为我正确地掌握了余单子的概念。(或者说,一个余单子的定义很简单;困难的部分在于理解它的实用性...)

我的问题是:某物件能够既是单子 又是 余单子吗?

我预见到两种可能的答案:

  • 是的,这很常见且广泛有用。
  • 不,它们执行着如此不同的任务,以至于没有理由希望某物体同时拥有两者的特质。

那么究竟是哪一种呢?


12
好的,“Identity”是一个简单的例子。 - Matvey Aksenov
3
ReaderWriter都可以作为共函子,不过它们的功能是不同的,并且需要一个Monoid约束条件来交换。 - C. A. McCann
1
非空列表是另一个例子。 - hammar
2
嗨,Haskell大佬们,现在是你们展示的时候了!走出评论区开始解释吧!我希望能得到一两个书面答案。Comonads通常被人们(包括我)理解不够透彻,这种比较性的问题会很有启发性。 - AndrewC
2
@MathematicalOrchid,虽然有人问得更多,但你的问题数量也很可观,而且质量很高。你的问题通常非常有趣,我有时只是浏览它们以学习有趣的东西。请继续提问。数据来源:当您单击任何haskell标签时,会出现一个top users链接。 - AndrewC
显示剩余8条评论
5个回答

27

自由余函子可以得到一些数据结构,它们既可以作为单子使用,也可以作为余单子使用:

data Cofree f a = a :< f (Cofree f a)

每个基于 Alternative 函子的自由余单子都可以产生一个单子 -- 可以在这里看到实例:

http://hackage.haskell.org/packages/archive/free/3.4.1/doc/html/Control-Comonad-Cofree.html

instance Alternative f => Monad (Cofree f) where
  return x = x :< empty
  (a :< m) >>= k = case k a of
                     b :< n -> b :< (n <|> fmap (>>= k) m)

例如,这使我们可以将非空列表作为Monad和Comonad(以及非空的f-分支树等)。

Identity不是替代方案,但是Cofree Identity生成无限流,事实上我们可以给该流提供一个不同的monad实例:

http://hackage.haskell.org/packages/archive/streams/3.1/doc/html/Data-Stream-Infinite.html

data Stream a = a :> Stream a
instance Comonad Stream where
  duplicate = tails
  extend f w = f w :> extend f (tail w)
  extract = head

instance Monad Stream where
  return = repeat
  m >>= f = unfold (\(bs :> bss) -> (head bs, tail <$> bss)) (fmap f m)

(请注意以上函数不是在列表中定义的,而是在streams包中定义的)。

同样,阅读器箭头并非替代品,但Cofree ((->) r)产生了一个Moore机,而Moore机也是单子和余单子:

http://hackage.haskell.org/packages/archive/machines/0.2.3.1/doc/html/Data-Machine-Moore.html

data Moore a b = Moore b (a -> Moore a b)
instance Monad (Moore a) where
  return a = r where r = Moore a (const r)
  Moore a k >>= f = case f a of
    Moore b _ -> Moore b (k >=> f)
  _ >> m = m
instance Comonad (Moore a) where
  extract (Moore b _) = b
  extend f w@(Moore _ g) = Moore (f w) (extend f . g)

那么这些例子背后的直觉是什么呢?我们可以免费获得共函子操作。我们获得的单子操作都是对角化的形式。使用"alternative",我们可以将事物 <|> 在一起来"压缩"结构,并在我们用完结构时创造出"空"的事物。这让我们可以处理有限情况。如果缺乏"alternative",我们需要有不确定数量的结构,这样无论我们进行多少次"join"操作(我们可以将其视为拼接或替换),总会有更多的空间来放置拼接的元素(就像希尔伯特酒店一样:http://www.encyclopediaofmath.org/index.php/Hilbert_infinite_hotel)。

相关地,每一个共函子都引出了一个相关的单子(尽管我认为这更像是一个好奇心):

http://hackage.haskell.org/packages/archive/kan-extensions/3.1.1/doc/html/Control-Monad-Co.html

http://comonad.com/reader/2011/monads-from-comonads/


14
“每个基于Alternative函子的Cofree余单子都会产生一个Monad” - 这堪比“Monad只是一个endo函子的幺半群”这句话所拥有的名望! :-D - MathematicalOrchid
“Alternative f => Monad (Cofree f)”不使用任何“Applicative”,也不使用“pure”或“<*>”。它只利用一个事实,即对于所有的“a”,“f a”都是一个monoid。 - Cirdec
True。它应该在任何具有适当诱导单调结构的函子上工作! - sclv

24
是的。 将一些评论转化为回答:
newtype Identity a = Identity {runIdenity :: a} deriving Functor
instance Monad Identity where
  return = Identity
  join = runIdentity
instance CoMonad Identity where
  coreturn = runIdentity
  cojoin = Identity

阅读器和写入器是一对完全的对偶,如下所示

class CoMonoid m where
  comempty :: (m,a) -> a
  comappend :: m -> (m,m)
--every haskell type is a CoMonoid
--that is because CCCs are boring!

instance Monoid a => Monad ((,) a) where
  return x = (mempty,x)
  join (a,(b,x)) = (a <> b, x)
instance CoMonoid a => CoMonad ((,) a) where
  coreturn = comempty
  cojoin = associate . first comappend

instance CoMonoid a => Monad ((->) a) where
  return = flip (curry comempty)
  join f = uncurry f . comappend
instance Monoid a => CoMonad ((->) a)  where
  coreturn f = f mempty
  cojoin f a b = f (a <> b)

我认为你的实现有些不正确,但它们背后的思路是正确的。 - C. A. McCann
3
我非常喜欢这样的对称性。就像在物理学中,如果事物是对称的,那意味着你找到了什么线索。 - Tikhon Jelvis
2
我刚刚真诚地笑了出来,因为i)这些符号名称(flip curry comempty)和ii)我自己对它们的理解非常少。这本身就值得点赞。 - Tom W
3
如果有人像我一样感到困惑,CCC代表笛卡尔闭范畴(我认为)。 - Clay Thomas

17

有许多有趣的结构既是Monad又是Comonad

已经有几个人在这里指出了Identity函子,但还有一些非平凡的例子。

Writer Monad在扮演Comonad时类似于Reader

instance Monoid e => Monad ((,) e)
instance Comonad ((,) e)

Reader Monad 具有类似于 Writer 的角色,作为一个 Comonad

instance Monad ((->) e)
instance Monoid e => Comonad ((->)e)

非空列表也构成单子和余单子,实际上是包含自由余单子的更大构造的特殊情况。 Identity 情况也可以看作是这种情况的一个特例。

还有各种基于Kan扩展的类似于 YonedaCodensity 的构造,用于转换单子和余单子,尽管在操作效率方面它们更偏向于其中之一。

我还有一个适配器,可以将任意余单子转换为单子变换器。不幸的是,在Haskell中相反的转换是不可能的。

在线性代数中有一个双代数(bialgebra)的概念。理想情况下,如果我们有一个既构成 Monad 又构成 Comonad 的东西,并且希望同时使用这些操作而无需逐个进行推理,那么我们希望 returnjoin 是余单子的余代数,因此 extractduplicate 就是 Monad 代数。如果满足这些条件,那么你实际上可以推理关于同时具有 Monad fComonad f 约束,并混合来自每个约束的组合器而无需逐个推理的代码。


你已经几次讨论过那个适配器了。它在 kan-extensions 中吗? - J. Abrahamson
是的。http://hackage.haskell.org/package/kan-extensions-4.1.0.1/docs/Control-Monad-Co.html - Edward Kmett

5

这取决于你认为“单子(monad)”是什么。如果你问“是否可能一个类型同时是MonadComonad的实例?”,那么答案是肯定的。这里有一个简单的例子。

newtype Id a = Id a

instance Monad Identity where
  return       = Id
  (Id a) >>= f = f a

instance Comonad Identity where
  extract (Id a) = a
  extend f ida = Id (f ida)

如果从数学上来讲,一个单子(Monad)是由三个元素 (X, return, bind) 组成的,其中 X 是一个类型,returnbind 遵循你所期望的类型和规则。类似地,一个余子(Comonad)就是 (X, extend, extract)。我刚才演示了可能有相同的X,但由于extendreturnextractbind的不同类型,它们不可能是相同的函数。因此,一个数学上的单子永远不可能是一个余子。

哎呀,Philip JF的代码示例比我的漂亮多了。 - J. Abrahamson
是的...我太过于追求细节了。 - J. Abrahamson

1

在Hammer的建议基础上,编写一个函数 [x] -> [[x]] 看起来非常简单。例如:

map (\ x -> [x])

看起来列表可以形成一个共同函子。哦,等等。这处理了cojoin,但是coreturn :: [x] -> x怎么办?显然,这就是为什么只有非空列表形成共同函子的原因。

这给我们带来了一个类型为([x] -> x) -> [x] -> [x]的cobind函数。有趣的是,Hoogle并不知道这样的函数。然而,我们已经有了concatMap :: (x -> [x]) -> [x] -> [x]。我还没有看到cobind函数的立即用途,但我可以想象存在这样的用途。

我仍然在努力理解共同函子以及它可能有用的地方。到目前为止,得到的答案让我有些思考......


1
您的cojoin不符合共单子律。特别地,coreturn.cojoin应该是恒等式。我谈到的共单子具有cojoin=init.tailscoreturn=head - hammar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接