应用函子的((<*>))定义?

时间:2018-12-13 13:51:03

标签: haskell applicative

一些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)ida -> 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}}-我无法解释。


有人可以一步一步解释它是如何工作的,为什么还要编译? 附言如有必要,欢迎使用分类理论术语。

4 个答案:

答案 0 :(得分:8)

对于问题1,您忽略了一个非常重要的上下文。

class Functor f => Applicative f where
    {-# MINIMAL pure, ((<*>) | liftA2) #-}

您引用的那些定义属于一个类。这意味着实例可以覆盖它们。此外,MINIMAL用法说明说,为了起作用,实例中至少必须覆盖其中之一。因此,只要在特定实例中重写递归,就会发生递归中断。就像Eq类彼此定义(==)(/=)的方式一样,因此您只需要为手写实例中的一个提供定义。

对于第二个问题,a -> b -> ca -> (b -> c)的简写。因此,它与d -> d统一为(b -> c) -> (b ->c)(为了避免冲突,我们将其重命名为变量)。 (相切地,这也是($)的类型。)

三个人-你是绝对正确的。继续简化!

\x -> (<*>) (fmap id x)
\x -> (<*>) x
(<*>)

所以ghci给您(<*>)的类型,这真的不是一个惊喜吗?

答案 1 :(得分:5)

  

1)(<*>)liftA2定义,其中liftA2(<*>)定义。它是如何工作的?我看不到明显的“递归中断”情况...

这不是递归。在Applicative的实例中,您既可以定义它们,也可以只定义一个。如果仅定义(<*>),则liftA2是从(<*>)定义的,反之亦然。

  

2)ida -> 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函数以及至少两个其他函数之一。

  

ida -> 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

这不太正确;对于某些xf 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 -> (<*>) xf(直到现在并不重要的某些小问题),尤其是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的“身份”。最好将后者可视化为二进制函数(应用程序),但可以等效地将其视为函数类型上的一元函数(身份)。