只有一个参数函数有效吗? Haskell

Is having only one argument functions efficient? Haskell

我已经开始学习 Haskell 并且我读到 haskell 中的每个函数都只有一个参数,我无法理解 Haskell 的幕后发生了什么魔法使之成为可能,我想知道它是否有效。

例子


>:t (+)
(+) :: Num a => a -> a -> a

上面的签名意味着 (+) 函数接受一个 Num 然后 returns 另一个接受一个 Num 和 returns 一个 Num 的函数


示例 1 相对简单,但我开始想知道当函数稍微复杂一点时会发生什么。

我的问题


为了示例,我编写了一个 zipWith 函数并以两种方式执行它,一次一次传递一个参数,一次传递所有参数。

zipwithCustom f (x:xs) (y:ys) = f x y : zipwithCustom f xs ys
zipwithCustom _ _ _ = []
zipWithAdd = zipwithCustom (+)
zipWithAddTo123 = zipWithAdd [1,2,3]

test1 = zipWithAddTo123 [1,1,1]
test2 = zipwithCustom (+) [1,2,3] [1,1,1]

>test1
[2,3,4]
>test2
[2,3,4]
  1. 一次传递一个参数 (scenario_1) 与一次传递所有参数一样高效 (scenario_2)?
  2. 这些场景在 Haskell 实际计算 test1test2 方面是否有任何不同(除了 scenario_1 可能需要更多内存作为它需要保存 zipWithAddzipWithAdd123)
  3. 这是正确的吗?为什么?在 scenario_1 中,我遍历 [1,2,3] 然后遍历 [1,1,1]
  4. 这是正确的吗?为什么?在 scenario_1scenario_2 我同时遍历两个列表

我意识到我在一个 post 中问了很多问题,但我相信这些是相互关联的,并且会帮助我(以及其他 Haskell 的新手)更好地理解什么Haskell 实际上正在发生,这使得这两种情况都成为可能。

你问的是“Haskell”,但是Haskell语言规范并不关心这些细节。选择如何进行评估取决于实现——规范唯一说明的是评估结果应该是什么,并小心避免给出必须用于计算该结果的算法。所以在这个答案中,我将讨论 GHC,实际上,它是唯一现存的实现。

对于 (3) 和 (4),答案很简单:无论您一次对一个参数应用 zipWithCustom 还是一次对所有参数应用 zipWithCustom,迭代模式都完全相同。 (并且该迭代模式是同时迭代两个列表。)

不幸的是,(1) 和 (2) 的答案复杂

起点是以下简单算法:

  1. 当您将函数应用于参数时,会创建(分配和初始化)闭包。闭包是内存中的数据结构,包含指向函数的指针和指向参数的指针。执行函数 body 时,任何时候提及其参数时,都会在闭包中查找该参数的值。
  2. 就是这样。

但是,这个算法有点糟糕。这意味着如果你有一个 7 参数的函数,你分配了 7 个数据结构,当你使用一个参数时,你可能必须遵循 7 长的指针链才能找到它。总的。所以 GHC 做了一些更聪明的事情。它以一种特殊的方式使用你的程序的语法:如果你将一个函数应用于多个参数,它只为该应用程序生成一个闭包,有多少参数就有多少字段。

(嗯...这可能不完全正确。实际上,它跟踪每个函数的 arity —— 以句法方式再次定义为使用的参数数量在定义该函数时 = 符号的左侧。如果您将函数应用于比其元数更多的参数,您可能会得到多个闭包或其他东西,我不确定。)

这非常好,因此您可能会认为与 test2 相比,您的 test1 会分配一个额外的闭包。你是对的...当优化器未打开时。

但是 GHC 也做了很多优化工作,其中之一就是注意“小”定义并内联它们。几乎可以肯定,启用优化后,您的 zipWithAddzipWithAddTo123 都会在使用它们的任何地方内联,我们将回到只分配一个闭包的情况。

希望这个解释能让您自己回答问题 (1) 和 (2),但如果没有,这里有明确的答案:

  1. Is passing one argument at the time as efficient as passing all arguments at once?

也许吧。一次传递一个参数可能会通过内联转换为一次传递所有参数,当然它们将是相同的。在没有这种优化的情况下,与一次传递所有参数相比,一次传递一个参数会有(非常轻微的)性能损失。

  1. Are those scenarios any different in terms of what Haskell is actually doing to compute test1 and test2?

test1test2 几乎肯定会被编译成相同的代码——甚至可能只编译其中一个而另一个是它的别名。

如果您想阅读更多关于实现中的想法,Spineless Tagless G-machine 论文比它的标题所暗示的更平易近人,只是有点过时了。