适用的仿函数评估对我来说还不清楚

时间:2019-03-02 18:26:00

标签: haskell functional-programming applicative

我目前正在阅读《学习Haskell,以获取美好的事物!并为评估某个代码块而费解。我已经阅读了好几次解释,并且开始怀疑甚至连作者都明白这段代码在做什么。

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

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

4 个答案:

答案 0 :(得分:6)

它将应用实例用于函数。您的代码

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

被评估为

( (\a->b->a+b) <$> (\c->c+3) <*> (\d->d*100) ) 5
( (\x -> (\a->b->a+b) ((\c->c+3) x)) <*> (\d->d*100) ) 5
( (\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
(\y -> ((\x->b->(x+3)+b) y) ((\d->d*100) y)) 5
(\y -> (b->(y+3)+b) (y*100)) 5
(\y -> (y+3)+(y*100)) 5
(5+3)+(5*100)

其中<$>fmap或仅仅是函数组合.,而<*>ap,如果您知道它在单子上的表现的话。

答案 1 :(得分:6)

另外两个答案给出了如何计算的详细信息-但我想我可能会提出一个更“直观”的答案来解释如何无需进行详细的计算就可以“看到”结果必须是508。

正如您所暗示的,每个Applicative(实际上甚至每个Functor)都可以被视为一种特定的“上下文”,其中包含给定类型的值。作为简单的例子:

  • Maybe a是一个上下文,其中可能存在类型a的值,但可能不存在(通常是由于某种原因而失败的计算结果)
  • [a]是一个上下文,可以容纳零个或多个类型a的值,并且没有上限的数量-表示特定计算的所有可能结果
  • IO a是一个上下文,其中a类型的值由于以某种方式与“外界”交互而可用。 (好吧,这不是那么简单...)

并且与此示例相关:

  • r -> a是一个上下文,其中类型a的值可用,但是尚不知道其特定值,因为它取决于类型{{1 }}。

根据此类上下文中的值,可以很好地理解r方法。 Applicative在“默认上下文”中嵌入一个“普通值”,该默认值在该上下文中的行为与“无上下文”的尽可能接近。对于上面的4个示例,我不会逐一介绍(其中大多数都是非常明显的),但是我会注意到,对于函数pure-即“纯值” pure = const由无论源值如何总是产生a的函数表示。

尽管我不想讲究如何使用“上下文”隐喻来最好地描述a,但我想讲的是特定的表达式:

<*>

其中f <$> a <*> b 是2个“纯值”之间的函数,而fa是“上下文中的值”。该表达式实际上具有一个同义词:liftA2。尽管通常认为使用b函数比使用liftA2<$>的“应用样式”更惯用,但该名称强调该思想是“提升”一个基于“普通值”的函数到“上下文中的值”。并且当想到这样的时候,我认为通常在给定特定的“上下文”(即特定的<*>实例)的情况下这样做是非常直观的。

所以表达式:

Applicative
对于适用性(+) <$> a <*> b ,类型为a的值bf Int

对于不同的实例f表现如下:

  • 如果f,则如果f = Maybea均为b值,则结果是将基础值加起来并将它们包装在{{1 }}。如果JustJusta,则整个表达式为b
  • 如果Nothing(列表实例),则上面的表达式是一个列表,其中包含形式为Nothing的所有和,其中f = []位于a' + b'和{{1}中}位于a'中。
  • 如果a,则上面的表达式是一个IO操作,它执行b'的所有I / O效果,然后执行b的I / O效果,并得出{ {1}}是由这两个动作产生的。

那么,如果f = IO是函数实例,那该怎么办?由于ab都是描述如何在给定(Int)输入的情况下如何获取给定f的函数,因此自然而然地提升a函数在它们上面应该是一个函数,该函数在得到输入的情况下会同时获得bInt函数的结果,然后将结果相加。

当然,这就是它的作用-以及其他答案已经很好地勾勒出了它的显式路线。但是它之所以如此工作的原因-实际上,正是我们拥有Int实例的原因,否则它看起来似乎很武断(尽管实际上这是很少的事情之一,如果不是唯一的话) ,将进行类型检查),以使实例与“根据给定函数依赖于某些尚未知的其他值的值”的语义匹配。总的来说,我想说这样“高层次”思考通常比被迫深入了解如何精确执行计算的低层次细节更好。 (尽管我当然不想低估也能够做到的重要性。)

[实际上,从哲学的角度来看,更确切地说,定义是正确的,只是因为类型检查是“自然的”定义,而实例恰好是巧合,具有如此好的“含义”。当然,数学充满了如此快乐的“巧合”,事实证明它们背后有很深的原因。]

答案 2 :(得分:5)

我们首先来看一下如何为一个函数定义fmap(<*>)

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

答案 3 :(得分:3)

对于任何应用程序,

        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)

(此答案的长版。)

纯数学没有时间。 Pure Haskell没有时间。动词说话(“应用函子应用”等)可能会造成混淆(“ 应用...何时? ...”)。

相反,(<*>)是一种组合器,它将带有功能(在某些情况下为 )的“计算”(由应用函子表示)组合在一起,将值(在类似上下文中的 )携带到一个组合的“计算”中,该运算将功能应用于该值(在这种情况下的 )。

“计算”用于将其与纯Haskell“计算”进行对比。 “计算”本身可能是纯粹的,也可能不是纯粹的,这是一个正交的问题。但是主要的意思是,“计算”体现了通用函数调用协议。他们可能会在将函数应用到其参数上时,作为/的一部分,来“做”某事。或输入类型,

    ( $ ) ::   (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

确实

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

  

最小完整定义

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完全相同)

h <$> g = pure h <*> g是每个应用函子都必须遵循的法律。

最后,

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

因为<*>位于左侧:infixl 4 <*>