代码之家  ›  专栏  ›  技术社区  ›  Zazaeil

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

  •  5
  • Zazaeil  · 技术社区  · 6 年前

    一些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 定义如下: (& lt;*& gt;) . 它是如何工作的?我看不到明显的“递归中断”情况…

    2) id 是一个 a -> a 功能。为什么传给 LIFTA2 作为一个 (a -> b -> c) 功能?

    3) fmap id x 永远平等 x ,因为函数必须保留适当的标识。因此 (<*>) (fmap id x) = (<*>) (x) 哪里 X = f a -安 a -类型化函数本身(顺便说一下,如何 -函数的类型化可以从纯范畴理论的观点来解释?函数只是类别之间的映射,它没有进一步的“类型化”…似乎最好说“一个类型的容器” 为所选类别的每个实例定义了(endo)函数 Hask 定义明确的haskell类型)。所以 (<*>) (f a) 而根据定义 (& lt;*& gt;) 期待 f(a' -> b') :因此,使其发挥作用的唯一方法是蓄意约束 成为一个 (a' -> b') . 但是当我跑步的时候 :t \x -> (<*>) (fmap id x) gchi 它吐出了一些令人兴奋的东西: f (a -> b) -> f a -> f b -我没有解释。


    有人能一步一步地解释它是如何工作的,以及它为什么会编译吗? 如有需要,欢迎使用P.S.类别理论术语。

    4 回复  |  直到 6 年前
        1
  •  8
  •   Carl    6 年前

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

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

    您引用的那些定义属于一个类。这意味着实例可以覆盖它们。此外,最小的pragma说,为了工作,必须在实例中至少重写其中一个。所以每当在一个特定的实例中重写一个递归时,就会中断递归。这就像 Eq 类定义 (==) (/=) 就彼此而言,这样您只需要在手写的实例中提供一个定义。

    对于第二个问题, a -> b -> c a -> (b -> c) . 所以它与(让我们重命名变量以避免冲突)相统一 d -> d 作为 (b -> c) -> (b ->c) . (相切地说,这也是 ($) )

    三个人-你说得对。继续简化!

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

    所以GHCI给你的 (<*>) 回来,是吗?

        2
  •  5
  •   Jorge Adriano Branco Aires    6 年前

    1) (<*>) 定义如下: liftA2 在哪里 LIFTA2 定义如下: (& lt;*& gt;) . 它是如何工作的?我看不到明显的“递归中断”情况…

    这不是递归。在你的例子中 Applicative 您可以同时定义它们,也可以只定义一个。如果你只是定义 (& lt;*& gt;) 然后 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
    

    三。

    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 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 
    
        3
  •  4
  •   Daniel Wagner    6 年前

    1) (<*>) 定义如下: liftA2 在哪里 LIFTA2 定义如下: (& lt;*& gt;) . 它是如何工作的?我看不到明显的“递归中断”情况…

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

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

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

    id 是一个 a -> a 功能。为什么传给 LIFTA2 作为一个 (a -> b -> c) 功能?

    如果 id :: a -> a 我们可以选择 a ~ d -> e 得到 id :: (d -> e) -> d -> e . 传统上,这种特殊的专业化 身份证件 拼写 ($) --也许你以前见过那个!

    3)…

    我不。。。事实上,你所陈述的事实中有任何矛盾。所以我不知道如何为你解释这个矛盾。然而,你的符号中有一些不符合的地方,可能与你思考中的错误有关,所以让我们简单地谈谈它们。

    你写的

    因此 (<*>) (fmap id x) = (<*>) (x) 哪里 x = f a .

    这并不完全正确;因为 类型 属于 X 甲A 对于一些 Functor f 但不一定 等于 甲A .

    顺便问一下,怎么可能 a -函数的类型化可以从纯范畴理论的观点来解释?函数只是类别之间的映射,它没有进一步的“类型化”…似乎更好的说法是——“为定义良好的haskell类型的假定类别hask的每个实例定义了(endo)函数的类型A的容器”

    函数由两部分组成:从对象到对象的映射,以及从箭头到与对象映射兼容的箭头的映射。在哈斯克尔 Functor 实例声明类似

    instance Functor F where fmap = fmapForF
    

    这个 F 是从对象到对象的映射(源和目标类别中的对象都是类型,以及 f 是一个接受一个类型并产生一个类型的东西)并且 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 (直到一些现在不重要的琐事),所以特别是 \x -> (<*>) (fmap id x) = (<*>) .所以ghci给了你 (& lt;*& gt;) ,应该如此。

        4
  •  2
  •   chi    6 年前

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

    我想说一个人不应该写

    ap = liftA2 id
    

    但是,相反,使用等价物

    ap = liftA2 ($)
    

    因为后者明确表示我们正在取消应用程序操作。

    (实际上,由于技术原因,GHC DEV不能使用 $ 正如下面评论中指出的,在这个内部模块中。所以,至少他们有很好的理由做出选择。)

    现在,你可能想知道为什么 id 可代替 $ . 正式地说,我们

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

    因此,ETA合同 x 然后 f 我们得到 ($) = 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
    

    因此,主要区别在于: 身份证件 标识是否在任何类型上 a ,同时 (美元) 是任何功能类型上的“标识” b -> c .后者最好被视为二进制函数(应用程序),但它可以等效地被视为函数类型上的一元函数(标识)。