【问题标题】:`(<*>)` definition for the Applicative functor?Applicative 函子的`(<*>)` 定义?
【发布时间】:2018-12-13 13:51:02
【问题描述】:

一些 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) (&lt;*&gt;) 是根据liftA2 定义的,其中liftA2 是根据(&lt;*&gt;) 定义的。它是如何工作的?我没有看到明显的“递归中断”案例......

2) id 是一个 a -&gt; a 函数。为什么它作为(a -&gt; b -&gt; c) 函数传递给liftA2

3) fmap id x 总是等于x,因为函子必须保持适当的身份。因此(&lt;*&gt;) (fmap id x) = (&lt;*&gt;) (x) where x = f a - 一个a 类型的函子本身(顺便说一句,a 函子的典型化如何可以从纯范畴论的角度来解释view? functor 只是类别之间的映射,它没有进一步的“典型化”......似乎更好的说法是 -“a 类型的容器,为每个 asummed 类别的实例定义了一个 (endo)functor @ 987654340@ 的定义明确的 Haskell 类型)。所以(&lt;*&gt;) (f a) 而根据定义(&lt;*&gt;) 期望f(a' -&gt; b'):因此,使其工作的唯一方法是故意将a 绑定为(a' -&gt; b')。但是当我在gchi 中运行:t \x -&gt; (&lt;*&gt;) (fmap id x),它吐出一些令人兴奋的东西:f (a -&gt; b) -&gt; f a -&gt; f b - 我无法解释。


有人可以逐步解释它是如何工作的以及为什么它甚至可以编译吗? 附言如果需要,欢迎使用范畴理论术语。

【问题讨论】:

    标签: haskell applicative


    【解决方案1】:

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

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

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

    对于问题二,a -&gt; b -&gt; ca -&gt; (b -&gt; c) 的简写。所以它与(让我们重命名变量以避免冲突)d -&gt; d 统一为(b -&gt; c) -&gt; (b -&gt;c)。 (切线,这也是($)的类型。)

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

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

    所以 ghci 给你返回 (&lt;*&gt;) 的类型并不奇怪,不是吗?

    【讨论】:

    • 关于第二个。所以a -&gt; (b -&gt; c) 有一个特殊情况——a 实际上是(b -&gt; c),对吧?然后它看起来像:a = (b -&gt; c) -&gt; (b -&gt; c).
    • @SerejaBogolubov 反过来:a -&gt; a 可以与其他类型统一,例如a = b -&gt; c,使得a -&gt; a = (b -&gt; c) -&gt; (b -&gt; c)
    【解决方案2】:

    1) (&lt;*&gt;) 是根据liftA2 定义的,其中liftA2 是根据(&lt;*&gt;) 定义的。它是如何工作的?我没有看到明显的“递归中断”案例......

    这不是递归。在Applicative 的实例中,您可以同时定义它们,也可以只定义一个。如果您只定义(&lt;*&gt;),那么liftA2 是从(&lt;*&gt;) 定义的,反之亦然。

    2) id 是一个 a -&gt; a 函数。为什么它作为(a -&gt; b -&gt; c) 函数传递给liftA2

    统一工作如下,

    (<*>) :: 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&lt;*&gt;

    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 &lt;*&gt; u = u的属性

      u <*> v 
    = liftA2 id u v 
    = pure id <*> u <*> v
    = u <*> v
    

    第二个我们需要liftA2 的属性。 applicative 的属性通常以pure&lt;*&gt; 的形式给出,所以我们需要先推导它。所需公式来源于pure h &lt;*&gt; 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 -&gt; a -&gt; b -&gt; 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 
    

    【讨论】:

    • 您的类型推断中似乎有一个错字:当您声明(&lt;*&gt;) fmap h x :: f c(仅在“编辑”部分之外)时,您可能是指(&lt;*&gt;) fmap h x :: (f b -&gt; f c)
    • @SerejaBogolubov 行动!固定的。谢谢!
    • bth,很好的一致性证明。是你自己写的还是来自一些科学论文?如果是的话,你能不能也分享一个?
    • 谢谢!不,我只是把它写下来。第一部分,搜索“类型推断”和“统一”,第二部分你需要知道applicative laws,然后是一些代数操作。
    【解决方案3】:

    1) (&lt;*&gt;) 是根据liftA2 定义的,其中liftA2 是根据(&lt;*&gt;) 定义的。它是如何工作的?我没有看到明显的“递归中断”案例......

    每个实例负责覆盖这两个实例中的至少一个。这在类顶部的 pragma 中以机器可读的方式记录:

    {-# MINIMAL pure, ((<*>) | liftA2) #-}
    

    此 pragma 声明实例编写者必须至少定义 pure 函数和其他两个函数中的至少一个。

    id 是一个a -&gt; a 函数。为什么将它作为(a -&gt; b -&gt; c) 函数传递给liftA2

    如果id :: a -&gt; a,我们可以选择a ~ d -&gt; e得到id :: (d -&gt; e) -&gt; d -&gt; e。传统上,id 的这种特殊专业化拼写为 ($)——也许你以前见过这个!

    3) ...

    我没有...实际上看到您陈述的事实中存在任何矛盾。所以我不知道如何为你解释这个矛盾。不过,你的记号有一些不妥之处,可能与你的思维错误有关,所以让我们简单谈谈。

    你写

    因此(&lt;*&gt;) (fmap id x) = (&lt;*&gt;) (x) 其中x = f a

    这不太对;对于某些Functor fx类型f a,但不一定等于f a

    顺便问一下,函子的a-typifying 怎么能从纯范畴论的角度来解释呢?函子只是类别之间的映射,它没有进一步的“典型化”......似乎最好说 - “一个类型为 a 的容器,为每个假设类别 Hask 的实例定义一个(endo)函子定义良好Haskell 类型

    函子由两部分组成:从对象到对象的映射,以及从箭头到与对象映射兼容的箭头的映射。在 Haskell Functor 实例声明中,如

    instance Functor F where fmap = fmapForF
    

    F 是对象到对象的映射(源类别和目标类别中的对象都是类型,F 是接受类型并产生类型的事物),fmapForF 是映射从箭头到箭头。

    我在 gchi 中运行 :t \x -&gt; (&lt;*&gt;) (fmap id x),它吐出一些令人兴奋的东西:f (a -&gt; b) -&gt; f a -&gt; f b - 我无法解释。

    嗯,你已经观察到fmap id x = x,这意味着\x -&gt; (&lt;*&gt;) (fmap id x) = \x -&gt; (&lt;*&gt;) x。对于任何功能ff = \x -&gt; f x(直到现在不重要的一些小问题),尤其是\x -&gt; (&lt;*&gt;) (fmap id x) = (&lt;*&gt;)。所以 ghci 应该为您提供(&lt;*&gt;) 的类型。

    【讨论】:

      【解决方案4】:

      在这里,我不得不不同意 GHC 开发人员的编码风格 :)

      我想说一个人永远不应该写

      ap = liftA2 id
      

      但是,改为使用等效

      ap = liftA2 ($)
      

      因为后者明确表明我们正在解除应用程序操作。

      (实际上,由于技术原因,GHC 开发人员不能在此内部模块中使用$,如下面的 cmets 中指出的那样。因此,至少他们有很好的选择理由。)

      现在,您可能想知道为什么可以使用id 而不是$。正式地,我们有

      ($) f x
      = f x
      = (id f) x
      = id f x
      

      因此,eta-contracting x 然后f,我们得到($) = 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 -&gt; c 上的“标识”。后者最好可视化为二元函数(应用程序),但也可以等效地视为函数类型上的一元函数(恒等式)。

      【讨论】:

      猜你喜欢
      • 2019-11-23
      • 1970-01-01
      • 2022-10-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-06-30
      • 1970-01-01
      相关资源
      最近更新 更多