Haskell 中 Zap Functor 和 zap 函数的用途是什么?

What is a purpose of Zap Functor and zap function in Haskell?

我在 Haskell 中遇到了 this construction。我找不到任何关于如何在实际代码中使用 zap/zapWithbizap/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 顺便说一句。 ReaderCoreader 分别同构于 ((->) 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中,如果fg在数学意义上是对偶的,那么下面的属性成立:

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 和 (,) 的双重双函子性质以及从 fg 的对偶性中继承的 zap,在我们的示例中将始终如此是 IdentityIdentity 以便 "unpeel" 离开 FreeCofree 的一层并递归调用该较低层上的 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。