【问题标题】:Haskell - Evaluation of (+) <$> (+3) <*> (*100) $ 5Haskell - 评估 (+) <$> (+3) <*> (*100) $ 5
【发布时间】:2015-07-01 03:25:37
【问题描述】:

From the chapter on Functors in Learn You a Haskell for Great Good,Lipovača 说:

“当我们执行(+) &lt;$&gt; (+3) &lt;*&gt; (*100) 时,我们正在创建一个函数,它将在(+3)(*100) 的结果上使用+ 并返回它。为了演示一个真实的例子,当我们执行@987654326 时@,5首先被应用到(+3)(*100),产生8500。然后,+8500调用,产生508。 "

但是,如果我尝试自己评估函数,考虑到函子 ((->) r) 上的 Applicative 的这个定义:

instance Applicative ((->) r) where  
    pure x = (\_ -> x)  
    f <*> g = \x -> f x (g x)  

我将上述表达式的评估读为:

(\x -&gt; (3 + x) (100 * x)) $ 5

但我不明白我们如何将两个部分应用的二进制函数组合为一个 lambda(事实上,GHCi 会抛出一个无限类型错误,试图将它绑定到一个变量)。此外,根据工作解释,如果我们查看 &lt;$&gt; 的类型定义,我们会得到:

(&lt;$&gt;) :: Functor f =&gt; (a -&gt; b) -&gt; f a -&gt; f b

或者更具体地说,我们可以将其提升视为:

(&lt;$&gt;) :: Functor f =&gt; (a -&gt; b) -&gt; (f a -&gt; f b)

考虑到在这种情况下我们的函子是 ((->) r),我可以推断这就是在之前的评估中发生的转换(假设左关联首先发生,而不是 @987654341 的右关联应用@):

(\x -&gt; a + b) 其中a = (+ 3)b = (* 100)。这是应该返回的函数。但是,我假设这是最终(粗略)形式是否正确?

(\x -&gt; (3 + x) + (100 * x)) $ 5

...产生 508。

我发现 Lipovača 的描述在表达式如何工作方面更容易理解,但我的直觉告诉我,对于 Haskell 编译器引擎盖下的血腥细节来说,这并不完全正确。我更容易认为 (+) 的 fmap 首先发生导致具有两个函子的函数,这两个函子是接受共享输入的部分应用函数,然后我们对其应用了一个值。由于惰性评估,我们可以这样做。这是错的吗?

【问题讨论】:

    标签: haskell functor applicative


    【解决方案1】:

    首先,请注意&lt;$&gt;&lt;*&gt; 都关联到左侧。内部没有什么神奇的事情发生,我们可以看到本质上是一系列 eta 扩展和 beta 减少的转变。一步一步,它看起来像这样:

    (((+) <$> (+3))         <*> (*100)) $ 5        -- Add parens
    ((fmap (+) (+3))        <*> (*100)) $ 5        -- Prefix fmap
    (((+) . (+3))           <*> (*100)) $ 5        -- fmap = (.)
    ((\a -> (+) ((+3) a))   <*> (*100)) $ 5        -- Definition of (.)
    ((\a -> (+) (a+3))      <*> (*100)) $ 5        -- Infix +
    ((\a b -> (+) (a+3) b)) <*> (*100)) $ 5        -- Eta expand
    (\x -> (\a b -> (+) (a+3) b) x ((*100) x)) $ 5 -- Definition of (<*>)
    (\x -> (\a b -> (+) (a+3) b) x (x*100)) $ 5    -- Infix *
    (\a b -> (+) (a + 3) b) 5 (5*100)              -- Beta reduce
    (\a b -> (a + 3) + b)   5 (5*100)              -- Infix +
    (5 + 3) + (5*100)                              -- Beta reduce (twice)
    508                                            -- Definitions of + and *
    

    有点令人困惑的是,$ 关联到右边这一事实与这里发生的事情的关系比它的固定性为 0 的事实要少。如果我们定义一个新的运算符,我们可以看到这一点:

    (#) :: (a -> b) -> a -> b
    f # a = f a
    infixl 0 #
    

    在 GHCi 中:

    λ> (+) <$> (+3) <*> (*100) # 5
    508
    

    【讨论】:

    • 非常感谢,这正是我想要的。我将不得不更多地研究 Eta 和 Beta 扩展和缩减以供将来阅读。尽管$ 是右关联的,但它被应用于一个thunk(将所有发生在$ a thunk 左侧的映射都称为安全吗?)因此所有这些都会被评估,对吧?
    • @RJS 你可以想到f $ a“表现得像”(f) (a) 其中fa 是可以有空格的代码位(虽然它并不总是那样,尤其是当$s 被链接时,但我经常这样想)。我添加了一些关于固定性的内容,我可能应该对此进行扩展。顺便说一句,实际上有一个(非常好的)论点可以让$ 左关联。我不确定成为一个笨蛋有什么关系。你这是什么意思?
    • 啊,我的语义错了;所有未评估的参数都是 thunk 的糟糕假设就是全部。
    • @RJS 好吧,未评估的参数是一个 thunk 是正确的(除非它被完全优化,也许)。某些东西是 thunk 的事实通常只在查看性能、无限大的结构、无限循环或产生错误的表达式时才重要。特别是,在没有无限事物和错误/异常的情况下,thunk 不会改变事物的含义(性能特征之外)。
    • 顺便说一下,eta 扩展/缩减和 beta 缩减是 lambda 演算中的术语。本质上,eta 扩展意味着采用表达式f(其中f 是一个函数)并将其转换为\x -&gt; f x。 Eta 减少则相反。 Beta 减少正在减少功能应用程序:(\x -&gt; f x) a 变成了f a。变量捕获有一些微妙之处,但在制作编译器或解释器时这是最重要的。重要的是要记住,当您手动执行此操作时,您应该使用新变量,以免发生名称冲突。