Haskell 中 Zap Functor 和 zap 函数的用途是什么?
What is a purpose of Zap Functor and zap function in Haskell?
我在 Haskell 中遇到了 this construction。我找不到任何关于如何在实际代码中使用 zap
/zapWith
和 bizap
/bizapWith
的示例或解释。它们是否以某种方式与标准 zip
/zipWith
函数相关?如何在 Haskell 代码中使用 Zap
/Bizap
仿函数?它们有什么好处?
Zap
涉及对偶函子。在 Haskell 中,这意味着一个携带额外的信息,另一个使用它,这就是他们俩所做的一切。 (r, a)
是一个带有额外r值的a值,r -> b
是一个首先需要一个r值的b值。 zapWith (+) read ("123", 321)
将把“123”塞进read,把123和321塞进(+)。如果您找到一段适合的代码,Zap
所做的只是帮助插入。 zap = uncurry
顺便说一句。 Reader
和 Coreader
分别同构于 ((->) r)
和 ((,) r)
。其他实例是一个提供两个值,另一个使用两个值,或者 f 提供一个 r,然后用完一个 s,而 g 提供一个 s,然后用完一个 r。
精彩的 Kmett 博客 post、Cofree Comonad and the Expression Problem 对此进行了介绍。不幸的是,该博客中有很多术语post,我自己也不是很了解所有内容,但我们可以尝试勾勒出细节。
皮亚诺数字
让我们用一种非常奇怪的方式来定义自然数。我们将首先定义 Peano 零函数和后继函数:
zero :: ()
zero = ()
incr :: a -> Identity a
incr = Identity
然后我们可以定义一些数字:
one :: Identity ()
one = incr zero
two :: Identity (Identity ())
two = incr . incr $ zero
three :: Identity (Identity (Identity ()))
three = incr . incr . incr $ zero
奇怪,但它似乎有效。您可以将 3 :: Int
转换为 three
并返回。 (试一试。)我们可以编写一个函数 f
将任意数字转换为我们的奇怪表示形式并返回吗?抱歉不行。 Haskell 类型系统不允许我们构造一个 无限类型 ,这正是我们想要 f
.
的类型
一个更大的问题是,作为一个懒惰的函数式程序员,我希望停止多次输入 Identity
。按照这个速度,我将被迫输入 O(n^2) 次来定义 n 个数字。这是 2016 年,我觉得这是不可接受的。
我们可以求助于 Free 数据类型来解决我们的两个问题。 (有些人称其为 Free monad,但我们没有理由说 "monad",而我们只能说 "type"。)
newtype Free f a = Free (Either a (f (Free f a)))
zero :: a -> Free Identity a
zero x = Free (Left x)
incr :: Free Identity a -> Free Identity a
incr = Free . Right . Identity
one :: a -> Free Identity a
one x = incr (zero x)
two :: a -> Free Identity a
two x = incr (incr (zero x))
three :: a -> Free Identity a
three = incr . incr . incr . zero
漂亮。这(也许令人惊讶)与我们上面不寻常的 Identity
-wrapping 表示相同。
流
现在让我们尝试建立一个流。比如说,从 2000 年开始的世纪闰年流(2000、2400、2800)。但是以一种奇怪的方式。
unfold :: (a -> a) -> a -> (a, (a, (a, ...)))
unfold a2a a = (a, unfold a2a (a2a a))
centurials :: (Int, (Int, (Int, ...)))
centurials = unfold (+ 400) 2000
假设编译器允许我们写下无限类型,这将是数字流的合适表示。 Cofree 来救援,"dual" 类型为 Free。类别理论意义上的双重,如果你花时间拿出一本类别理论教科书,煮了很多咖啡,画出分类图,然后翻转所有箭头,你会从一个到另一个(或另一个)。这个糟糕的解释足以作为关于二元性的手波浪段落。
newtype Cofree f a = Cofree (a, f (Cofree f a))
unfold :: Functor f => (a -> f a) -> a -> Cofree f a
unfold a2fa a = Cofree (a, fmap (unfold a2fa) (a2fa a))
centurials :: Cofree Identity Int
centurials = unfold (Identity . (+ 400)) 2000
这(也许再次令人惊讶)等同于我们上面的无限俄罗斯嵌套娃娃表示。
用 Peano 数字索引流
但是如何查找流中的特定元素呢?通过利用 Free 和 Cofree 之间的二元性,我们实际上可以使用我们的 Peano 数表示来索引我们的流表示。
事实证明,在Haskell中,如果f
和g
在数学意义上是对偶的,那么下面的属性成立:
class Zap f g | f -> g, g -> f where
zap :: (a -> b -> r) -> f a -> g b -> r
我们将不得不省略关于对偶性的讨论以及为什么这个 属性 对对偶函子成立。
我们可以实现最简单的实例:恒等函子(在 Haskell 中表示为 newtype Identity a = Identity a
)和它自身之间的数学对偶。
instance Zap Identity Identity where
zap ab2r (Identity a) (Identity b) = ab2r a b
这个属性也可以扩展到双函子:
class Bizap f g | f -> g, g -> f where
bizap :: (a -> c -> r) -> (b -> d -> r) -> f a b -> g c d -> r
我们可以实例化这个 class 用于 Haskell 的和与积的编码,它们在范畴论中是(非平凡的!)对偶:
instance Bizap Either (,) where
bizap ac2r bd2r (Left a) (c, d) = ac2r a c
bizap ac2r bd2r (Right b) (c, d) = bd2r b d
instance Bizap (,) Either where
bizap ac2r bd2r (a, b) (Left c) = ac2r a c
bizap ac2r bd2r (a, b) (Right d) = bd2r b d
我们现在有足够的机器来显示 Haskell Free 和 Cofree 之间的相同二元性。
instance Zap f g => Zap (Cofree f) (Free g) where
zap ab2r (Cofree as) (Free bs) = bizap ab2r (zap (zap ab2r)) as bs
instance Zap f g => Zap (Free f) (Cofree g) where
zap ab2r (Free as) (Cofree bs) = bizap ab2r (zap (zap ab2r)) as bs
这些实例利用了 Either 和 (,) 的双重双函子性质以及从 f
和 g
的对偶性中继承的 zap
,在我们的示例中将始终如此是 Identity
和 Identity
以便 "unpeel" 离开 Free
和 Cofree
的一层并递归调用该较低层上的 zap
。
最后,让我们看看实际效果:
year2800 :: Int
year2800 = zap id (two id) centurials
通过利用此 zap 或 "annhilation" 属性,我们能够使用 Free 构建的自然数索引从 Cofree 构建的流中检索值。尽管与真实世界的示例相去甚远,但该代码作为一个练习存在,说明我们如何将类别理论中的高错误思想编码为 Haskell 类型和类型 classes。我们能够做到这一点既可以作为脑筋急转弯,也可以作为我们对 Free 和 Cofree 类型选择的合理性检查。
您可能会发现这些实例对单行代码很有用,或者当您的数据结构恰好排列得恰到好处时,如 Gurkenglas 的回答中所述。如果您发现对偶性是一个有用的 属性 可以利用,那么一定要使用 category-extras 包的这一部分。但即使我们找不到它的用法,我们也一定能欣赏到一切都整齐地组合在一起的美。
至于压缩
You are correct in thinking that there is a connection between Zap and Zip。它们以文字游戏驱动开发的 Kmett 风格命名。编码 zippable 仿函数导致非常相似的类型 class Zip
:
class Functor f => Zip f where
fzipWith :: (a -> b -> c) -> f a -> f b -> f c
您可以按照此构造为树和流(再次使用 Cofree)导出通用压缩函数。有关详细信息,请参阅博客 post。
我在 Haskell 中遇到了 this construction。我找不到任何关于如何在实际代码中使用 zap
/zapWith
和 bizap
/bizapWith
的示例或解释。它们是否以某种方式与标准 zip
/zipWith
函数相关?如何在 Haskell 代码中使用 Zap
/Bizap
仿函数?它们有什么好处?
Zap
涉及对偶函子。在 Haskell 中,这意味着一个携带额外的信息,另一个使用它,这就是他们俩所做的一切。 (r, a)
是一个带有额外r值的a值,r -> b
是一个首先需要一个r值的b值。 zapWith (+) read ("123", 321)
将把“123”塞进read,把123和321塞进(+)。如果您找到一段适合的代码,Zap
所做的只是帮助插入。 zap = uncurry
顺便说一句。 Reader
和 Coreader
分别同构于 ((->) r)
和 ((,) r)
。其他实例是一个提供两个值,另一个使用两个值,或者 f 提供一个 r,然后用完一个 s,而 g 提供一个 s,然后用完一个 r。
精彩的 Kmett 博客 post、Cofree Comonad and the Expression Problem 对此进行了介绍。不幸的是,该博客中有很多术语post,我自己也不是很了解所有内容,但我们可以尝试勾勒出细节。
皮亚诺数字
让我们用一种非常奇怪的方式来定义自然数。我们将首先定义 Peano 零函数和后继函数:
zero :: ()
zero = ()
incr :: a -> Identity a
incr = Identity
然后我们可以定义一些数字:
one :: Identity ()
one = incr zero
two :: Identity (Identity ())
two = incr . incr $ zero
three :: Identity (Identity (Identity ()))
three = incr . incr . incr $ zero
奇怪,但它似乎有效。您可以将 3 :: Int
转换为 three
并返回。 (试一试。)我们可以编写一个函数 f
将任意数字转换为我们的奇怪表示形式并返回吗?抱歉不行。 Haskell 类型系统不允许我们构造一个 无限类型 ,这正是我们想要 f
.
一个更大的问题是,作为一个懒惰的函数式程序员,我希望停止多次输入 Identity
。按照这个速度,我将被迫输入 O(n^2) 次来定义 n 个数字。这是 2016 年,我觉得这是不可接受的。
我们可以求助于 Free 数据类型来解决我们的两个问题。 (有些人称其为 Free monad,但我们没有理由说 "monad",而我们只能说 "type"。)
newtype Free f a = Free (Either a (f (Free f a)))
zero :: a -> Free Identity a
zero x = Free (Left x)
incr :: Free Identity a -> Free Identity a
incr = Free . Right . Identity
one :: a -> Free Identity a
one x = incr (zero x)
two :: a -> Free Identity a
two x = incr (incr (zero x))
three :: a -> Free Identity a
three = incr . incr . incr . zero
漂亮。这(也许令人惊讶)与我们上面不寻常的 Identity
-wrapping 表示相同。
流
现在让我们尝试建立一个流。比如说,从 2000 年开始的世纪闰年流(2000、2400、2800)。但是以一种奇怪的方式。
unfold :: (a -> a) -> a -> (a, (a, (a, ...)))
unfold a2a a = (a, unfold a2a (a2a a))
centurials :: (Int, (Int, (Int, ...)))
centurials = unfold (+ 400) 2000
假设编译器允许我们写下无限类型,这将是数字流的合适表示。 Cofree 来救援,"dual" 类型为 Free。类别理论意义上的双重,如果你花时间拿出一本类别理论教科书,煮了很多咖啡,画出分类图,然后翻转所有箭头,你会从一个到另一个(或另一个)。这个糟糕的解释足以作为关于二元性的手波浪段落。
newtype Cofree f a = Cofree (a, f (Cofree f a))
unfold :: Functor f => (a -> f a) -> a -> Cofree f a
unfold a2fa a = Cofree (a, fmap (unfold a2fa) (a2fa a))
centurials :: Cofree Identity Int
centurials = unfold (Identity . (+ 400)) 2000
这(也许再次令人惊讶)等同于我们上面的无限俄罗斯嵌套娃娃表示。
用 Peano 数字索引流
但是如何查找流中的特定元素呢?通过利用 Free 和 Cofree 之间的二元性,我们实际上可以使用我们的 Peano 数表示来索引我们的流表示。
事实证明,在Haskell中,如果f
和g
在数学意义上是对偶的,那么下面的属性成立:
class Zap f g | f -> g, g -> f where
zap :: (a -> b -> r) -> f a -> g b -> r
我们将不得不省略关于对偶性的讨论以及为什么这个 属性 对对偶函子成立。
我们可以实现最简单的实例:恒等函子(在 Haskell 中表示为 newtype Identity a = Identity a
)和它自身之间的数学对偶。
instance Zap Identity Identity where
zap ab2r (Identity a) (Identity b) = ab2r a b
这个属性也可以扩展到双函子:
class Bizap f g | f -> g, g -> f where
bizap :: (a -> c -> r) -> (b -> d -> r) -> f a b -> g c d -> r
我们可以实例化这个 class 用于 Haskell 的和与积的编码,它们在范畴论中是(非平凡的!)对偶:
instance Bizap Either (,) where
bizap ac2r bd2r (Left a) (c, d) = ac2r a c
bizap ac2r bd2r (Right b) (c, d) = bd2r b d
instance Bizap (,) Either where
bizap ac2r bd2r (a, b) (Left c) = ac2r a c
bizap ac2r bd2r (a, b) (Right d) = bd2r b d
我们现在有足够的机器来显示 Haskell Free 和 Cofree 之间的相同二元性。
instance Zap f g => Zap (Cofree f) (Free g) where
zap ab2r (Cofree as) (Free bs) = bizap ab2r (zap (zap ab2r)) as bs
instance Zap f g => Zap (Free f) (Cofree g) where
zap ab2r (Free as) (Cofree bs) = bizap ab2r (zap (zap ab2r)) as bs
这些实例利用了 Either 和 (,) 的双重双函子性质以及从 f
和 g
的对偶性中继承的 zap
,在我们的示例中将始终如此是 Identity
和 Identity
以便 "unpeel" 离开 Free
和 Cofree
的一层并递归调用该较低层上的 zap
。
最后,让我们看看实际效果:
year2800 :: Int
year2800 = zap id (two id) centurials
通过利用此 zap 或 "annhilation" 属性,我们能够使用 Free 构建的自然数索引从 Cofree 构建的流中检索值。尽管与真实世界的示例相去甚远,但该代码作为一个练习存在,说明我们如何将类别理论中的高错误思想编码为 Haskell 类型和类型 classes。我们能够做到这一点既可以作为脑筋急转弯,也可以作为我们对 Free 和 Cofree 类型选择的合理性检查。
您可能会发现这些实例对单行代码很有用,或者当您的数据结构恰好排列得恰到好处时,如 Gurkenglas 的回答中所述。如果您发现对偶性是一个有用的 属性 可以利用,那么一定要使用 category-extras 包的这一部分。但即使我们找不到它的用法,我们也一定能欣赏到一切都整齐地组合在一起的美。
至于压缩
You are correct in thinking that there is a connection between Zap and Zip。它们以文字游戏驱动开发的 Kmett 风格命名。编码 zippable 仿函数导致非常相似的类型 class Zip
:
class Functor f => Zip f where
fzipWith :: (a -> b -> c) -> f a -> f b -> f c
您可以按照此构造为树和流(再次使用 Cofree)导出通用压缩函数。有关详细信息,请参阅博客 post。