我在Haskell遇到this construction。我找不到任何关于如何在实际代码中使用zap
/ zapWith
和bizap
/ bizapWith
的示例或说明。它们是否在某种程度上与标准zip
/ zipWith
函数相关?如何在Haskell代码中使用Zap
/ Bizap
仿函数?他们有什么好处?
答案 0 :(得分:17)
这篇精彩的Kmett博客文章Cofree Comonad and the Expression Problem涵盖了这一点。不幸的是,该博客文章中有很多术语,我自己也不太了解所有内容,但我们可以尝试勾勒出细节。
让我们以一种非常奇怪的方式定义自然数。我们将首先定义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数据类型以获取有关我们两个问题的帮助。 (有些人称之为自由单子,但我们没有理由说'#34; monad"当我们只能说"键入"。)
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
- 包装表示相同。
现在让我们尝试构建一个流。比如说,从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"输入免费。在类别理论意义上的双重性,如果你花时间并拿出一个类别理论教科书并煮了很多咖啡并抽出了分类图,然后翻转所有箭头,你会从一个到另一个(或者另一个)。这种糟糕的解释必须足以作为关于二元性的手势段落。
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
这(也许令人惊讶的是)这相当于我们上面无限的俄罗斯嵌套玩偶。
但是如何寻找流中的特定元素?通过利用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
此属性也可以扩展到bifunctors:
class Bizap f g | f -> g, g -> f where
bizap :: (a -> c -> r) -> (b -> d -> r) -> f a b -> g c d -> r
我们可以为Haskell对sum和product的编码实例化这个类,这在类别理论中是(非平凡的!)双重的:
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
我们现在有足够的机器在Free和Cofree之间的Haskell中显示相同的二元性。
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和(,)的双重bifunctor性质以及来自zap
和f
的二元性的继承g
,在我们的示例中将始终为Identity
{1}}和Identity
以便#34;取消"从Free
和Cofree
开始的图层,并在该较低图层上递归调用zap
。
最后,让我们看看它的实际效果:
year2800 :: Int
year2800 = zap id (two id) centurials
通过利用这种消息或者#34;消灭"我们能够使用由Free构建的自然数字索引从Cofree构建的流中检索值的属性。虽然远非现实世界的例子,但代码存在于我们如何编码高falutin' Haskell类型和类型类中的类别理论的想法。我们能够做到这一点是一个脑筋急转弯,也是我们选择免费和Cofree类型的理智检查。
您可能会发现这些实例对单行有用,或者您的数据结构恰好排列正确,如Gurkenglas'回答。如果您发现二元性是一个有用的属性可以利用,那么绝对可以达到类别附加包的这一部分。但即使我们找不到它的用法,我们当然也能欣赏到整齐地融合在一起的美丽。
You are correct in thinking that there is a connection between Zap and Zip。它们以Kmett的文字游戏开发风格命名。对zippable仿函数进行编码会产生类似的类型Zip
:
class Functor f => Zip f where
fzipWith :: (a -> b -> c) -> f a -> f b -> f c
您可以通过遵循此构造为树和流(再次使用Cofree)派生通用压缩函数。有关详细信息,请参阅博客文章。
答案 1 :(得分:5)
(r, a)
涉及双重函数。在Haskell中,这意味着一个人携带额外的信息而另一个人使用它,而这两者都是如此。 r -> b
是一个带有额外r值的值,zapWith (+) read ("123", 321)
是首先需要r值的b值。 Zap
会插入" 123"读取,并将123和321插入(+)。如果您找到适合的代码,那么zap = uncurry
所做的就是插件的帮助。顺便说一下,对于这个实例Reader
。 Coreader
和((->) r)
分别与((,) r)
和@if(Auth::user()->user_level == 1)
<span class="icon-bar"><a href="item/create">Create item</a></span>
@end
同构。其他实例将是一个提供两个值,另一个使用up,或者f提供r,然后使用s,而g提供s,然后用完r。