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
,演示
extract
extend
unwrap
duplicate
?
交叉发布: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 f
的Functor
实例
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 f
的 Foldable
实例,我们应该能够计算节点数。
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 move
s。游戏进行如下:
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)
我一直在玩 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
,演示
extract
extend
unwrap
duplicate
?
交叉发布: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 f
的Functor
实例
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 f
的 Foldable
实例,我们应该能够计算节点数。
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 move
s。游戏进行如下:
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)