Haskell 中 Cofree CoMonad 有哪些激励人心的例子?

What are some motivating examples for Cofree CoMonad in Haskell?

我一直在玩 Cofree,但不太理解它。

例如,我想在 ghci 中使用 Cofree [] Num,但找不到任何有趣的示例。

例如,如果我构造一个 Cofree 类型:

let a = 1 :< [2, 3]

我希望 extract a == 1,但我得到了这个错误:

No instance for (Num (Cofree [] a0)) arising from a use of ‘it’
    In the first argument of ‘print’, namely ‘it’
    In a stmt of an interactive GHCi command: print it

还有一种:

extract a :: (Num a, Num (Cofree [] a)) => a

我能得到一些简单的示例,甚至是微不足道的示例,以说明如何将 Cofree 与仿函数一起使用:[],或 Maybe,或 Either,演示

交叉发布:https://www.reddit.com/r/haskell/comments/4wlw70/what_are_some_motivating_examples_for_cofree/

编辑:在 David Young 的评论的指导下,这里有一些更好的例子可以说明我的第一次尝试被误导的地方,但是我仍然喜欢一些可以指导 Cofree 直觉的例子:

> let a = 1 :< []
> extract a
    1
> let b = 1 :< [(2 :< []), (3 :< [])]
> extract b
    1
> unwrap b
    [2 :< [],3 :< []]
> map extract $ unwrap b
    [2,3]

让我们回顾一下 Cofree 数据类型的定义。

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

这至少足以诊断示例中的问题。当你写

1 :< [2, 3]

你犯了一个小错误,报告的内容比有用的更隐蔽。这里,f = []a 是数字,因为 1 :: a。相应的你需要

[2, 3] :: [Cofree [] a]

因此

2 :: Cofree [] a

如果 Cofree [] a 也是 Num 的实例,那么 可能 没问题。因此,您的定义获得了一个不太可能被满足的约束,实际上,当您使用您的值时,满足约束的尝试失败了。

再试一次

1 :< [2 :< [], 3 :< []]

而且你的运气应该会更好。

现在,让我们看看我们得到了什么。从保持简单开始。 Cofree f () 是什么? Cofree [] () 是什么?后者与 [] 的固定点同构:树结构,其中每个节点都是子树列表,也称为 "unlabelled rose trees"。例如,

() :< [  () :< [  () :< []
               ,  () :< []
               ]
      ,  () :< []
      ]

类似地,Cofree Maybe () 或多或少是 Maybe 的不动点:自然数的副本,因为 Maybe 给了我们零个或一个位置来插入一个子树。

zero :: Cofree Maybe ()
zero = () :< Nothing
succ :: Cofree Maybe () -> Cofree Maybe ()
succ n = () :< Just n

一个重要的小案例是 Cofree (Const y) (),它是 y 的副本。 Const y 函子为子树提供 no 个位置。

pack :: y -> Cofree (Const y) ()
pack y = () :< Const y

接下来,让我们开始讨论另一个参数。它会告诉您附加到每个节点的标签类型。更具暗示性地重命名参数

data Cofree nodeOf label = label :< nodeOf (Cofree nodeOf label)

当我们标记 (Const y) 示例时,我们得到

pair :: x -> y -> Cofree (Const y) x
pair x y = x :< Const y

当我们将标签附加到数字的节点时,我们得到 nonempty 列表

one :: x -> Cofree Maybe x
one = x :< Nothing
cons :: x -> Cofree Maybe x -> Cofree Maybe x
cons x xs = x :< Just xs

对于列表,我们得到标记的玫瑰树。

0 :< [  1 :< [  3 :< []
             ,  4 :< []
             ]
     ,  2 :< []
     ]

这些结构总是"nonempty",因为至少有一个顶级节点,即使它没有子节点,而且那个节点总是有一个标签。 extract操作给你顶节点的标签。

extract :: Cofree f a -> a
extract (a :< _) = a

也就是extract丢掉了top label的context

现在,duplicate 操作 装饰 每个标签 它自己的 上下文。

duplicate :: Cofree f a -> Cofree f (Cofree f a)
duplicate a :< fca = (a :< fca) :< fmap duplicate fca  -- f's fmap

我们可以通过访问整棵树

获得Cofree fFunctor实例
fmap :: (a -> b) -> Cofree f a -> Cofree f b
fmap g (a :< fca) = g a :< fmap (fmap g) fca
    --                     ^^^^  ^^^^
    --                 f's fmap  ||||
    --                           (Cofree f)'s fmap, used recursively

不难看出

fmap extract . duplicate = id

因为 duplicate 用其上下文装饰每个节点,然后 fmap extract 丢弃装饰。

请注意,fmap 只能查看输入的标签来计算输出的标签。假设我们想根据每个输入标签 在其上下文 中计算输出标签?例如,给定一棵未标记的树,我们可能希望用其整个子树的大小来标记每个节点。多亏 Cofree fFoldable 实例,我们应该能够计算节点数。

length :: Foldable f => Cofree f a -> Int

也就是说

fmap length . duplicate :: Cofree f a -> Cofree f Int

comonads 的关键思想是它们捕获 "things with some context",并且它们让您可以在任何地方应用上下文相关的映射。

extend :: Comonad c => (c a -> b) -> c a -> c b
extend f = fmap f       -- context-dependent map everywhere
           .            -- after
           duplicate    -- decorating everything with its context

更直接地定义 extend 可以省去重复的麻烦(尽管这仅相当于共享)。

extend :: (Cofree f a -> b) -> Cofree f a -> Cofree f b
extend g ca@(_ :< fca) = g ca :< fmap (extend g) fca

并且你可以通过

取回duplicate
duplicate = extend id -- the output label is the input label in its context

此外,如果您选择 extract 作为对上下文中每个标签所做的事情,您只需将每个标签放回原来的位置:

extend extract = id

这些"operations on labels-in-context"被称为"co-Kleisli arrows",

g :: c a -> b

extend 的工作是将联合 Kleisli 箭头解释为整个结构上的函数。 extract 操作是身份联合 Kleisli 箭头,它被 extend 解释为身份函数。当然还有合作的Kleisli作文

(=<=) :: Comonad c => (c s -> t) -> (c r -> s) -> (c r -> t)
(g =<= h) = g . extend h

并且共同法则确保 =<= 具有结合性并吸收 extract,从而为我们提供共同 Kleisli 范畴。此外我们还有

extend (g =<= h)  =  extend g . extend h

所以 extend 是一个 函子 (在分类意义上)从 co-Kleisli 范畴到集合和函数。这些定律不难检查 Cofree,因为它们遵循节点形状的 Functor 定律。

现在,一种在 cofree comonad 中查看结构的有用方法是作为一种 "game server"。一个结构

a :< fca

表示游戏状态。游戏中的一步由 "stopping" 组成,在这种情况下,您会得到 a,或者 "continuing",通过选择 fca 的子树。例如,考虑

Cofree ((->) move) prize

此服务器的客户端必须停止,或通过给出 move 继续:它是一个 list of moves。游戏进行如下:

play :: [move] -> Cofree ((->) move) prize -> prize
play []       (prize :< _) = prize
play (m : ms) (_     :< f) = play ms (f m)

可能一个move是一个Char,而prize是解析字符序列的结果

如果你仔细观察,你会发现 [move]Free ((,) move) () 的一个版本。自由 monad 代表客户端策略。仿函数 ((,) move) 相当于一个只有命令 "send a move" 的命令接口。函子((->) move)就是对应的结构体"respond to the sending of a move".

一些函子可以看作是捕获一个命令接口;这种仿函数的自由 monad 表示发出命令的程序;仿函数将有一个 "dual" 代表如何响应命令; dual 的 cofree comonad 是环境的一般概念,其中发出命令的程序可以是 运行,标签说明程序停止时要做什么,returns 是一个值,子结构说如果程序发出命令,如何 运行ning 程序。

例如,

data Comms x = Send Char x | Receive (Char -> x)

描述允许发送或接收字符。它的对偶是

data Responder x = Resp {ifSend :: Char -> x, ifReceive :: (Char, x)}

作为练习,看看你能不能实现交互

chatter :: Free Comms x -> Cofree Responder y -> (x, y)