只有一个参数函数有效吗? 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]
- 一次传递一个参数 (scenario_1) 与一次传递所有参数一样高效 (scenario_2)?
- 这些场景在 Haskell 实际计算
test1
和 test2
方面是否有任何不同(除了 scenario_1 可能需要更多内存作为它需要保存 zipWithAdd
和 zipWithAdd123
)
- 这是正确的吗?为什么?在 scenario_1 中,我遍历
[1,2,3]
然后遍历 [1,1,1]
- 这是正确的吗?为什么?在 scenario_1 和 scenario_2 我同时遍历两个列表
我意识到我在一个 post 中问了很多问题,但我相信这些是相互关联的,并且会帮助我(以及其他 Haskell 的新手)更好地理解什么Haskell 实际上正在发生,这使得这两种情况都成为可能。
你问的是“Haskell”,但是Haskell语言规范并不关心这些细节。选择如何进行评估取决于实现——规范唯一说明的是评估结果应该是什么,并小心避免给出必须用于计算该结果的算法。所以在这个答案中,我将讨论 GHC,实际上,它是唯一现存的实现。
对于 (3) 和 (4),答案很简单:无论您一次对一个参数应用 zipWithCustom
还是一次对所有参数应用 zipWithCustom
,迭代模式都完全相同。 (并且该迭代模式是同时迭代两个列表。)
不幸的是,(1) 和 (2) 的答案复杂。
起点是以下简单算法:
- 当您将函数应用于参数时,会创建(分配和初始化)闭包。闭包是内存中的数据结构,包含指向函数的指针和指向参数的指针。执行函数 body 时,任何时候提及其参数时,都会在闭包中查找该参数的值。
- 就是这样。
但是,这个算法有点糟糕。这意味着如果你有一个 7 参数的函数,你分配了 7 个数据结构,当你使用一个参数时,你可能必须遵循 7 长的指针链才能找到它。总的。所以 GHC 做了一些更聪明的事情。它以一种特殊的方式使用你的程序的语法:如果你将一个函数应用于多个参数,它只为该应用程序生成一个闭包,有多少参数就有多少字段。
(嗯...这可能不完全正确。实际上,它跟踪每个函数的 arity —— 以句法方式再次定义为使用的参数数量在定义该函数时 =
符号的左侧。如果您将函数应用于比其元数更多的参数,您可能会得到多个闭包或其他东西,我不确定。)
这非常好,因此您可能会认为与 test2
相比,您的 test1
会分配一个额外的闭包。你是对的...当优化器未打开时。
但是 GHC 也做了很多优化工作,其中之一就是注意“小”定义并内联它们。几乎可以肯定,启用优化后,您的 zipWithAdd
和 zipWithAddTo123
都会在使用它们的任何地方内联,我们将回到只分配一个闭包的情况。
希望这个解释能让您自己回答问题 (1) 和 (2),但如果没有,这里有明确的答案:
- Is passing one argument at the time as efficient as passing all arguments at once?
也许吧。一次传递一个参数可能会通过内联转换为一次传递所有参数,当然它们将是相同的。在没有这种优化的情况下,与一次传递所有参数相比,一次传递一个参数会有(非常轻微的)性能损失。
- Are those scenarios any different in terms of what Haskell is actually doing to compute
test1
and test2
?
test1
和 test2
几乎肯定会被编译成相同的代码——甚至可能只编译其中一个而另一个是它的别名。
如果您想阅读更多关于实现中的想法,Spineless Tagless G-machine 论文比它的标题所暗示的更平易近人,只是有点过时了。
我已经开始学习 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]
- 一次传递一个参数 (scenario_1) 与一次传递所有参数一样高效 (scenario_2)?
- 这些场景在 Haskell 实际计算
test1
和test2
方面是否有任何不同(除了 scenario_1 可能需要更多内存作为它需要保存zipWithAdd
和zipWithAdd123
) - 这是正确的吗?为什么?在 scenario_1 中,我遍历
[1,2,3]
然后遍历[1,1,1]
- 这是正确的吗?为什么?在 scenario_1 和 scenario_2 我同时遍历两个列表
我意识到我在一个 post 中问了很多问题,但我相信这些是相互关联的,并且会帮助我(以及其他 Haskell 的新手)更好地理解什么Haskell 实际上正在发生,这使得这两种情况都成为可能。
你问的是“Haskell”,但是Haskell语言规范并不关心这些细节。选择如何进行评估取决于实现——规范唯一说明的是评估结果应该是什么,并小心避免给出必须用于计算该结果的算法。所以在这个答案中,我将讨论 GHC,实际上,它是唯一现存的实现。
对于 (3) 和 (4),答案很简单:无论您一次对一个参数应用 zipWithCustom
还是一次对所有参数应用 zipWithCustom
,迭代模式都完全相同。 (并且该迭代模式是同时迭代两个列表。)
不幸的是,(1) 和 (2) 的答案复杂。
起点是以下简单算法:
- 当您将函数应用于参数时,会创建(分配和初始化)闭包。闭包是内存中的数据结构,包含指向函数的指针和指向参数的指针。执行函数 body 时,任何时候提及其参数时,都会在闭包中查找该参数的值。
- 就是这样。
但是,这个算法有点糟糕。这意味着如果你有一个 7 参数的函数,你分配了 7 个数据结构,当你使用一个参数时,你可能必须遵循 7 长的指针链才能找到它。总的。所以 GHC 做了一些更聪明的事情。它以一种特殊的方式使用你的程序的语法:如果你将一个函数应用于多个参数,它只为该应用程序生成一个闭包,有多少参数就有多少字段。
(嗯...这可能不完全正确。实际上,它跟踪每个函数的 arity —— 以句法方式再次定义为使用的参数数量在定义该函数时 =
符号的左侧。如果您将函数应用于比其元数更多的参数,您可能会得到多个闭包或其他东西,我不确定。)
这非常好,因此您可能会认为与 test2
相比,您的 test1
会分配一个额外的闭包。你是对的...当优化器未打开时。
但是 GHC 也做了很多优化工作,其中之一就是注意“小”定义并内联它们。几乎可以肯定,启用优化后,您的 zipWithAdd
和 zipWithAddTo123
都会在使用它们的任何地方内联,我们将回到只分配一个闭包的情况。
希望这个解释能让您自己回答问题 (1) 和 (2),但如果没有,这里有明确的答案:
- Is passing one argument at the time as efficient as passing all arguments at once?
也许吧。一次传递一个参数可能会通过内联转换为一次传递所有参数,当然它们将是相同的。在没有这种优化的情况下,与一次传递所有参数相比,一次传递一个参数会有(非常轻微的)性能损失。
- Are those scenarios any different in terms of what Haskell is actually doing to compute
test1
andtest2
?
test1
和 test2
几乎肯定会被编译成相同的代码——甚至可能只编译其中一个而另一个是它的别名。
如果您想阅读更多关于实现中的想法,Spineless Tagless G-machine 论文比它的标题所暗示的更平易近人,只是有点过时了。