一些Haskell源代码(请参见ref):
-- | Sequential application.
--
-- A few functors support an implementation of '<*>' that is more
-- efficient than the default one.
(<*>) :: f (a -> b) -> f a -> f b
(<*>) = liftA2 id
-- | Lift a binary function to actions.
--
-- Some functors support an implementation of 'liftA2' that is more
-- efficient than the default one. In particular, if 'fmap' is an
-- expensive operation, it is likely better to use 'liftA2' than to
-- 'fmap' over the structure and then use '<*>'.
liftA2 :: (a -> b -> c) -> f a -> f b -> f c
liftA2 f x = (<*>) (fmap f x)
三件事似乎让我很困惑:
1)(<*>)
用liftA2
定义,其中liftA2
用(<*>)
定义。它是如何工作的?我看不到明显的“递归中断”情况...
2)id
是a -> a
函数。为什么将它作为liftA2
函数传递给(a -> b -> c)
?
3)fmap id x
始终等于x
,因为函子必须保留适当的身份。因此(<*>) (fmap id x)
= (<*>) (x)
,其中x
= f a
-类型为a
的函子本身(顺便说一下,a
如何典型化函子可以从纯类别理论的角度来解释吗?函子只是类别之间的映射,它没有进一步的“典型化” ...似乎最好说-“ a
类型的容器并为定义良好的Haskell类型的汇总类别Hask
的每个实例定义了一个(endo)functor。因此,(<*>) (f a)
根据定义(<*>)
期望f(a' -> b')
:因此,使其生效的唯一方法是故意将a
绑定为(a' -> b')
。但是,当我在:t \x -> (<*>) (fmap id x)
中运行gchi
时,它会吐出令人惊讶的内容:{ {1}}-我无法解释。
有人可以一步一步解释它是如何工作的,为什么还要编译? 附言如有必要,欢迎使用分类理论术语。
答案 0 :(得分:8)
对于问题1,您忽略了一个非常重要的上下文。
class Functor f => Applicative f where
{-# MINIMAL pure, ((<*>) | liftA2) #-}
您引用的那些定义属于一个类。这意味着实例可以覆盖它们。此外,MINIMAL用法说明说,为了起作用,实例中至少必须覆盖其中之一。因此,只要在特定实例中重写递归,就会发生递归中断。就像Eq
类彼此定义(==)
和(/=)
的方式一样,因此您只需要为手写实例中的一个提供定义。
对于第二个问题,a -> b -> c
是a -> (b -> c)
的简写。因此,它与d -> d
统一为(b -> c) -> (b ->c)
(为了避免冲突,我们将其重命名为变量)。 (相切地,这也是($)
的类型。)
三个人-你是绝对正确的。继续简化!
\x -> (<*>) (fmap id x)
\x -> (<*>) x
(<*>)
所以ghci给您(<*>)
的类型,这真的不是一个惊喜吗?
答案 1 :(得分:5)
1)
(<*>)
用liftA2
定义,其中liftA2
用(<*>)
定义。它是如何工作的?我看不到明显的“递归中断”情况...
这不是递归。在Applicative
的实例中,您既可以定义它们,也可以只定义一个。如果仅定义(<*>)
,则liftA2
是从(<*>)
定义的,反之亦然。
2)
id
是a -> a
函数。为什么将它作为liftA2
函数传递给(a -> b -> c)
?
统一工作如下
(<*>) :: f (a -> b) -> f a -> f b (<*>) = liftA2 id liftA2 :: (a -> b -> c) -> f a -> f b -> f c
id : u -> u
liftA2 : (a -> (b -> c) -> f a -> f b -> f c
------------------------------------------------------
u = a
u = b->c
id : (b->c) -> (b->c)
liftA2 : ((b->c) -> (b->c)) -> f (b->c) -> f b -> f c
------------------------------------------------------
liftA2 id : f (b->c) -> f b -> f c
3。
liftA2 :: (a -> b -> c) -> f a -> f b -> f c liftA2 h x = (<*>) (fmap h x)
将第一个参数从f
重命名为h
,以防止混淆,因为f
也显示为类型
h :: a -> (b -> c)
x :: f a
fmap :: (a -> d) -> f a -> f d
------------------------------
d = b -> c
h :: a -> (b->c)
x :: f a
fmap :: (a -> (b->c)) -> f a -> f (b->c)
----------------------------------------
fmap h x :: f (b -> c)
fmap h x :: f (b -> c)
(<*>) :: f (b -> c) -> f b -> f c
-------------------------------------
(<*>) fmap h x :: f b -> f c
编辑:
一致性
要显示两个公式的一致性,首先让我们首先将liftA2
重写为更简单的形式。我们可以使用下面的公式来摆脱fmap
,而仅使用pure
和<*>
fmap h x = pure h <*> x
,最好将所有要点放入定义中。这样我们就可以
liftA2 h u v
= (<*>) (fmap h u) v
= fmap h u <*> v
= pure h <*> u <*> v
所以我们要检查
的一致性u <*> v = liftA2 id u v
liftA2 h u v = pure h <*> u <*> v
首先,我们需要pure id <*> u = u
u <*> v
= liftA2 id u v
= pure id <*> u <*> v
= u <*> v
第二步,我们需要一个属性liftA2
。应用属性通常以pure
和<*>
的形式给出,因此我们需要首先导出它。所需公式来自pure h <*> pure x = pure (h x)
。
liftA2 h (pure x) v
= pure h <*> pure x <*> v
= pure (h x) <*> v
= liftA2 (h x) v
这需要h : t -> a -> b -> c
。一致性证明变为
liftA2 h u v
= pure h <*> u <*> v
= pure h `liftA2 id` u `liftA2 id` v
= liftA2 id (liftA2 id (pure h) u) v
= liftA2 id (liftA2 h u) v
= liftA2 h u v
答案 2 :(得分:4)
1)
(<*>)
用liftA2
定义,其中liftA2
用(<*>)
定义。它是如何工作的?我看不到明显的“递归中断”情况...
每个实例至少负责覆盖两者之一。在类顶部的编译指示中以机器可读的方式对此进行了记录:
{-# MINIMAL pure, ((<*>) | liftA2) #-}
此用法说明宣布,实例编写者必须定义至少pure
函数以及至少两个其他函数之一。
id
是a -> a
函数。为什么将它作为liftA2
函数传递给(a -> b -> c)
?
如果为id :: a -> a
,我们可以选择a ~ d -> e
来获得id :: (d -> e) -> d -> e
。传统上,id
的这种特殊专业化是拼写为($)
的-也许您之前已经看过!
3)...
我没有……实际上您看到的事实中存在任何矛盾。所以我不确定如何为您解释矛盾。但是,您的记号中有些愚蠢之处可能与您的思维错误有关,因此让我们简单地谈谈它们。
您写
因此
(<*>) (fmap id x)
=(<*>) (x)
,其中x
=f a
。
这不太正确;对于某些x
,f a
的类型是Functor f
,但不一定等于 f a
。< / p>
顺便说一句,如何从纯范畴论的角度解释
a
的函子类型化? functor只是类别之间的映射,它没有进一步的“典型化” ...似乎更好地说:“类型为a的容器,其中为假定类别的每个实例定义了(endo)functor定义明确的Hask Haskell类型
函子由两部分组成:从对象到对象的映射以及与对象映射兼容的从箭头到箭头的映射。在Haskell Functor
实例声明中,例如
instance Functor F where fmap = fmapForF
F
是对象到对象的映射(源类别和目标类别中的对象都是类型,而F
是一种接受类型并产生类型的事物)和{{ 1}}是从箭头到箭头的映射。
我在gchi中跑
fmapForF
,它吐出令人难以置信的东西::t \x -> (<*>) (fmap id x)
-我无法解释。
好吧,您已经观察到f (a -> b) -> f a -> f b
,即fmap id x = x
。对于任何功能\x -> (<*>) (fmap id x) = \x -> (<*>) x
,f
(直到现在并不重要的某些小问题),尤其是f = \x -> f x
。因此,ghci会为您提供\x -> (<*>) (fmap id x) = (<*>)
的类型。
答案 3 :(得分:2)
在这里我不得不不同意GHC开发人员的编码风格:)
我想说一个人不应该写
ap = liftA2 id
但是,请使用等效的
ap = liftA2 ($)
因为后者清楚地表明我们正在取消应用程序操作。
(实际上,出于技术上的原因,GHC开发人员无法在此内部模块中使用$
,如下面的评论中所指出。因此,至少他们有很好的选择理由。)< / p>
现在,您可能想知道为什么可以使用id
代替$
。正式地,我们有
($) f x
= f x
= (id f) x
= id f x
因此,先进行x
然后再进行f
的eta合同,我们得到($) = id
。
实际上,($)
是id
的“特殊情况”。
id :: a -> a
-- choose a = (b -> c) as a special case
id :: (b -> c) -> (b -> c)
id :: (b -> c) -> b -> c
($):: (b -> c) -> b -> c
因此,主要区别在于:id
是任何类型a
的身份,而($)
是任何功能类型b -> c
的“身份”。最好将后者可视化为二进制函数(应用程序),但可以等效地将其视为函数类型上的一元函数(身份)。