Applicative Functor 评估我不清楚

Applicative functor evaluation is not clear to me

我目前正在阅读 Haskell 学习你的好书!并且在对某个代码块的评估进行解释时绊倒了。解释看了好几遍,开始怀疑连作者都看不懂这段代码是干什么的了

ghci> (+) <$> (+3) <*> (*100) $ 5
508

应用函子将某些上下文中的函数应用于某些上下文中的值,以获得某些上下文中的某些结果。我花了几个小时研究这个代码块,并对如何计算这个表达式提出了一些解释,其中 none 个是令人满意的。我知道 (5+3)+(5*100) 是 508,但问题在于这个表达式。有没有人对这段代码有明确的解释?

正在使用函数的应用实例。你的代码

(+) <$> (+3) <*> (*100) $ 5

被评估为

( (\a->\b->a+b) <$> (\c->c+3) <*> (\d->d*100) ) 5         -- f <$> g
( (\x -> (\a->\b->a+b) ((\c->c+3) x)) <*> (\d->d*100) ) 5 -- \x -> f (g x)
( (\x -> (\a->\b->a+b) (x+3)) <*> (\d->d*100) ) 5
( (\x -> \b -> (x+3)+b) <*> (\d->d*100) ) 5
( (\x->\b->(x+3)+b) <*> (\d->d*100) ) 5         -- f <*> g
(\y -> ((\x->\b->(x+3)+b) y) ((\d->d*100) y)) 5 -- \y -> (f y) (g y)
(\y -> (\b->(y+3)+b) (y*100)) 5
(\y -> (y+3)+(y*100)) 5
(5+3)+(5*100)

其中 <$>fmap 或者只是函数组合 .,而 <*>ap 如果你知道它在 monads 上的行为。

我们先来看看fmap and (<*>)是如何定义一个函数的:

instance Functor ((->) r) where
    fmap = (.)

instance Applicative ((->) a) where
    pure = const
    (<*>) f g x = f x (g x)
    liftA2 q f g x = q (f x) (g x)

我们要评估的表达式是:

 (+) <$> (+3) <*> (*100)  $ 5

或更详细:

((+) <$> (+3)) <*> (*100) $ 5

如果我们这样计算 (<$>),它是 fmap 的中缀同义词,我们就会看到它等于:

(+) . (+3)

所以这意味着我们的表达式等同于:

((+) . (+3)) <*> (*100) $ 5

接下来我们可以应用顺序应用。因此,这里 f 等于 (+) . (+3)g 等于 (*100)。因此,这意味着我们构建了一个如下所示的函数:

\x -> ((+) . (+3)) x ((*100) x)

我们现在可以将其简化并重写为:

\x -> ((+) (x+3)) ((*100) x)

然后重写为:

\x -> (+) (x+3) ((*100) x)

因此,我们构建了一个如下所示的函数:

\x -> (x+3) + 100 * x

或更简单:

\x -> 101 * x + 3

如果我们再计算:

(\x -> 101*x + 3) 5

那我们当然得到:

101 * 5 + 3

因此:

505 + 3

这是预期的:

508

其他两个答案已经详细说明了这是如何计算的——但我想我可能会补充更多 "intuitive" 答案来解释如何在不进行详细计算的情况下,可以 "see" 结果必须是 508.

正如您所暗示的,每个 Applicative(事实上,甚至每个 Functor)都可以被视为一种特定类型的 "context",它包含给定类型的值。作为简单的例子:

  • Maybe a 是一个上下文,其中 a 类型的值可能存在,但也可能不存在(通常是由于某种原因可能失败的计算结果)
  • [a] 是一个上下文,可以包含零个或多个 a 类型的值,数量没有上限 - 表示特定计算的所有可能结果
  • IO a 是一个上下文,其中类型 a 的值作为与 "the outside world" 以某种方式交互的结果可用。 (好吧,那不是那么简单...)

并且,与此示例相关:

  • r -> a 是一个上下文,其中 a 类型的值可用,但其特定值尚不清楚,因为它取决于某些(尚未知的)类型 r.

Applicative 方法在这种上下文中基于值可以很好地理解。 pure 将 "ordinary value" 嵌入到 "default context" 中,在该上下文中它的行为与 "context-free" 尽可能接近。我不会针对上面的 4 个示例中的每一个(其中大部分都非常明显),但我会注意到对于函数,pure = const - 即 "pure value" a 由始终生成 a 的函数表示,无论源值是什么。

我不想详述如何使用 "context" 隐喻来最好地描述 <*>,我想详述特定的表达方式:

f <$> a <*> b

其中f是2"pure values"和a之间的函数,b是"values in a context"。这个表达式实际上有一个同义词作为函数:liftA2。尽管使用 liftA2 函数通常被认为不如使用 <$><*> 的 "applicative style" 惯用,但该名称强调的是 "lift" 上的一个函数"ordinary values" 到 "values in a context" 上的一个。当这样想时,我认为给定一个特定的 "context"(即一个特定的 Applicative 实例),这通常是非常直观的。

所以表达式:

(+) <$> a <*> b

对于值 ab 的类型说 f Int 对于应用程序 f,对于不同的实例表现如下 f:

  • if f = Maybe,那么如果 ab 都是 Just 值,则结果是将基础值相加并将它们包装在 Just。如果 abNothing,则整个表达式是 Nothing.
  • if f = [](列表实例)则上面的表达式是一个列表,其中包含 a' + b' 形式的所有和,其中 a'ab'b.
  • if f = IO,则上面的表达式是一个 IO 动作,它执行 a 的所有 I/O 效果,然后是 b 的效果,并导致这两个动作产生的 Int 的总和。

那么,如果 f 是函数实例,最后它会做什么呢?由于 ab 都是描述如何在给定任意 (Int) 输入的情况下获得给定 Int 的函数,因此提升 (+) 函数是很自然的在它们之上的应该是函数,在给定输入的情况下,获取 ab 函数的结果,然后将结果相加。

当然,这就是它的作用 - 以及它所依据的明确路径已被其他答案非常巧妙地映射出来。但它为什么会这样工作的原因——事实上,这正是我们拥有 f <*> g = \x -> f x (g x) 实例的原因,否则它可能看起来相当随意(尽管实际上它是极少数事情之一,如果不是唯一的话) ,将进行类型检查),以便实例匹配 "values which depend on some as-yet-unknown other value, according to the given function" 的语义。总的来说,我会说这样想 "at a high level" 通常比被迫深入了解计算执行方式的低级细节要好。 (虽然我当然不想淡化也能够做到后者的重要性。)

[其实从哲学的角度来说,可能更准确的说法是定义如此,只是因为"natural"的定义进行了类型检查,纯属巧合然后该实例呈现出如此漂亮的 "meaning"。数学当然充满了这样的快乐 "coincidences" 这背后有很深的原因。]

对于任何应用程序,

        a <$> b <*> c  =  liftA2 a b c

对于函数,

    liftA2 a  b     c      x 
=          a (b x) (c x)          -- by definition;
=       (a . b) x  (c x)
=     ((a <$> b) <*> c)    x

因此

        (+) <$> (+3) <*> (*100)  $  5
=
  liftA2 (+)   (+3)      (*100)     5
=
         (+)  ((+3) 5)  ((*100) 5)
=
              (5+3)  +  (5*100)

(此答案的长版如下。)

纯数学没时间。纯Haskell没时间。用动词说话(“applicative functor applies”等)可能会造成混淆(“applies...when?...”)。

相反,(<*>) 是一个组合子,它组合了一个带有函数的“计算”(由应用函子表示)(在那种计算类型的上下文中 ) 和相同类型的“计算”,携带一个值( 在类似上下文 中),到一个组合的“计算”中,该计算执行该函数对该值的应用(在这种情况下).

“计算”是用来对比纯粹的Haskell“计算”(在 Philip Wadler 的“Calculating is better than Scheming”论文之后,它本身指的是 David Turner 的 Kent Recursive Calculator 语言,一个Haskell).

的(主要)前任 Miranda 的前任

“计算”本身可能是纯粹的,也可能不是纯粹的,这是一个正交问题。但它的主要意思是,“计算”体现了 通用函数调用协议 。除了/作为/执行函数对其参数的应用的一部分,他们可能会“做”一些事情。或者类型,

    ( $ ) ::   (a ->   b)  ->    a ->   b
    (<$>) ::   (a ->   b)  ->  f a -> f b
    (<*>) :: f (a ->   b)  ->  f a -> f b
    (=<<) ::   (a -> f b)  ->  f a -> f b

对于函数,上下文是应用程序(另一个一个),并且要恢复值——无论是函数还是参数——应用程序到一个普通的参数要执行。

(耐心等待,我们快到了)。

模式 a <$> b <*> c 也可以表示为 liftA2 a b c。因此,“函数”应用函子“计算”类型由

定义
   liftA2 h x y s  = let x' = x s            -- embellished application of h to x and y
                         y' = y s in         -- in context of functions, or Reader
                      h  x'    y'

-- liftA2 h x y    = let x' = x              -- non-embellished application, or Identity
--                       y' = y   in         
--                    h  x'    y'

-- liftA2 h x y s  = let (x',s')  = x s      -- embellished application of h to x and y
--                       (y',s'') = y s' in  -- in context of
--                   (h  x'    y', s'')      -- state-passing computations, or State

-- liftA2 h x y    = let (x',w)  = x         -- embellished application of h to x and y
--                       (y',w') = y  in     -- in context of
--                   (h  x'    y', w++w')    -- logging computations, or Writer

-- liftA2 h x y    = [h  x'    y' |          -- embellished application of h to x and y
--                             x' <- x,      -- in context of 
--                             y' <- y ]     -- nondeterministic computations, or List

--  ( and for Monads we define `liftBind h x k =` and replace `y` with `k x'`
--      in the bodies of the above combinators; then liftA2 becomes liftBind: )
--    liftA2   ::    (a -> b -> c) -> f a ->       f b  -> f c
--    liftBind ::    (a -> b -> c) -> f a -> (a -> f b) -> f c
--    (>>=) = liftBind (\a b -> b) :: f a -> (a -> f b) -> f b

事实上,上面所有的片段都可以用 ApplicativeDo 写成 liftA2 h x y = do { x' <- x ; y' <- y ; pure (h x' y') } 或者更直观地写成 liftA2 h x y = [h x' y' | x' <- x, y' <- y],使用 Monad Comprehensions,因为上述所有计算类型都是 monad 以及应用函子。顺便说一句,这表明 (<*>) = liftA2 ($),这可能也很有启发性。

的确如此,

> :t let liftA2 h x y r = h (x r) (y r) in liftA2
       :: (a -> b -> c)  -> (t -> a)  -> (t -> b)  -> (t -> c)

> :t liftA2   -- the built-in one
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c

即当我们取 f a ~ (t -> a) ~ (->) t a 时类型匹配,即 f ~ (->) t.

因此,我们已经完成:

       (+) <$> (+3) <*> (*100)  $ 5
=
liftA2 (+)    (+3)      (*100)    5
=
       (+)   ((+3) 5)  ((*100) 5)
=
       (+)   (5+3)     (5*100)
=
             (5+3)  +  (5*100)

这就是liftA2这种类型定义的方式,Applicative ((->) t) => ...:

instance Applicative ((->) t) where
    pure     x   t =    x
    liftA2 h x y t = h (x t) (y t)

不需要定义(<*>)The source code says:

Minimal complete definition

pure, ((<*>) | liftA2)

所以现在你想问很久了,为什么是不是a <$> b <*> c就等于liftA2 a b c

简短的回答是,就是这样。一个可以根据另一个来定义——即 (<*>) 可以通过 liftA2

来定义
    g <*> x    = liftA2 id  g     x        -- i.e. (<*>) = liftA2 id = liftA2 ($)

-- (g <*> x) t = liftA2 id  g     x t 
--               =      id (g t) (x t) 
--               =    (id . g) t (x t)     -- = (id <$> g <*> x) t
--               =          g  t (x t)

(与in the source定义的完全一样),

也是每个Applicative Functor都必须遵守的定律,那就是h <$> g = pure h <*> g

最后,

liftA2 h g x  ==  pure h <*> g <*> x
--     h g x  ==      (h     g)    x

因为<*>联想到左边:是infixl 4 <*>.