【问题标题】:Is having only one argument functions efficient? Haskell只有一个参数函数有效吗?哈斯克尔
【发布时间】:2021-01-07 20:37:25
【问题描述】:

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

示例


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

上面的签名意味着(+)函数接受一个Num然后返回另一个函数接受一个Num并返回一个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 方面是否有任何不同(除了场景_1 可能需要更多内存,因为它需要保存 zipWithAddzipWithAdd123)李>
  3. 这是正确的吗?为什么?在scenario_1中,我遍历[1,2,3],然后遍历[1,1,1]
  4. 这是正确的吗?为什么?在 scenario_1scenario_2 我同时遍历两个列表

我意识到我在一篇文章中提出了很多问题,但我相信这些问题是相互关联的,并且会帮助我(以及其他刚接触 Haskell 的人)更好地了解 Haskell 中实际发生的事情,从而使这两种情况都成为可能.

【问题讨论】:

  • 在这两种情况下,您同时遍历两个列表。
  • test2 中你也每次都用一个参数调用它,因为zipWithCustom (+) [1,2,3] [1,1,1]((zipWithCustom (+)) [1,2,3]) [1,1,1] 的缩写。
  • @hdw3:它不运行该功能。 haskell 是 懒惰的。它只会在必要时执行某些操作,因此当您枚举列表时,您已经传递了 f 和列表。
  • @hdw3:但更重要的是,这个想法是,如果您调用 zipWithCustom (+),您只需构造一个将获取列表的 new 函数,然后再构造一个将获取另一个列表。所以zipWith (+)基本上是构造一个new函数,而不是一个列表。

标签: performance function haskell arguments


【解决方案1】:

您询问“Haskell”,但语言规范中的 Haskell 并不关心这些细节。由实现来选择评估的发生方式——规范中唯一说的是评估的结果应该是什么,并且小心地避免给出必须用于计算该结果的算法。所以在这个答案中,我将讨论 GHC,实际上,它是唯一现存的实现。

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

很遗憾,(1) 和 (2) 的答案复杂

起点是以下简单算法:

  1. 将函数应用于参数时,会创建(分配和初始化)闭包。闭包是内存中的数据结构,包含指向函数的指针和指向参数的指针。当函数体执行时,只要提到它的参数,就会在闭包中查找该参数的值。
  2. 就是这样。

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

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

这很好,因此您可能会认为您的test1test2 相比会分配一个额外的闭包。你是对的......当优化器没有打开时。

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

希望这种解释能让您自己回答问题 (1) 和 (2),但以防万一,以下是对这些问题的明确答案:

  1. 一次传递一个参数是否与一次传递所有参数一样有效?

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

  1. 就 Haskell 实际计算 test1test2 而言,这些场景有什么不同吗?

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

如果您想了解更多关于实施中的想法,Spineless Tagless G-machine 论文比其标题所暗示的要平易近人得多,而且只是有点过时了。

【讨论】:

  • 最重要的是,在 OP 示例中,“(非常轻微的)性能损失”仅在整个列表中出现一次,即使 zipwithCustom 多次使用闭包。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-10-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多