在haskell中函数定义的LHS中添加参数的语义是什么?

What are the semantics of adding the parameter in LHS of function definition in haskell?

我是 haskell 的初学者,正在努力理解 Let vs Where wiki page。最后有一个示例,其中在函数定义 fib 的左侧添加参数 x 会更改语义。

fib1 =
   let fib' 0 = 0
       fib' 1 = 1
       fib' n = fib1 (n - 1) + fib1 (n - 2)
   in  (map fib' [0 ..] !!)

fib2 x =
   let fib' 0 = 0
       fib' 1 = 1
       fib' n = fib2 (n - 1) + fib2 (n - 2)
   in  map fib' [0 ..] !! x

维基页面指出 "In the second case [fib2], fib' is redefined for every argument x"。我正在寻找一个初学者友好的解释为什么会发生这种情况,一般来说,是否有更多这样的隐藏副作用?

维基页面也有一个 link 对 eta reduction 的解释,它指出表达式和它的 eta-reduction 是等价的。所以,如果 fib2fib1 的 eta 抽象,为什么它们不等价?

你的最后一点也许是最重要的一点 - fib1fib2 确实是 等价的,编译器完全可以自由转换一个变成另一个。由于此转换对指称语义没有影响,仅对操作语义有影响,因此通常将其称为优化,如果它 "improves" 程序的操作语义。问题是很难说出 "improves" 的真正含义。

维基页面指出

The compiler cannot know whether you intended this -- while it increases time complexity it may reduce space complexity.

这是真的 - 一般来说,"optimization" 可能不是优化,具体取决于您衡量性能的方式。因此,一般来说,编译器不会执行此优化,将这样做的负担留给程序员(如果他们愿意的话),除非真的确定它确实会提高性能代码。

区别在于一个叫做共享的特性。在 fib1 中,map fib' 中的 fib' 调用将在 fib' 函数内的 fib1 的不同调用之间共享。这意味着只要计算函数,就必须保留整个列表 map fib' [0..n]。在某些情况下,这可能对性能不利,但在这种情况下,它可以为您节省 很多 的递归函数调用。在 fib2 中,每个 map fib' 都是单独计算的,因为参数在 let 之外。考虑这个程序,即使没有优化,它也等同于 fib1

fib3 :: Int -> Integer 
fib3 =
   let fib' 0 = 0
       fib' 1 = 1
       fib' n = fib1 (n - 1) + fib1 (n - 2)
    in \x -> map fib' [0 ..] !! x

\x -> .. 的位置至关重要 - 这里是 let fib' = .. in \x -> map fib' .. 但在 fib2 中是 \x -> let fib' = .. in map fib' ...

Thus it will not float the definition out from under the binding of x.

这显然取决于您使用的具体编译器以及您如何编译程序!我假设这个 wiki 页面是在编译器不够聪明的时候编写的,无法找出这个特定的例子,但是使用 GHC 7.10.3 和 -O2,编译器实际上生成 相同 这两个程序的代码。

如果你在没有优化的情况下编译,或者解释,性能上的差异将会变得很明显。这不是由于单态限制 - 即使您为函数提供单态类型,它也存在:

fib1 :: Int -> Integer 
fib1 = ..

fib2 :: Int -> Integer 
fib2 x = ..

区别很明显:

>:set +s
>fib2 32
2178309
(10.86 secs, 6,063,137,456 bytes)
>fib1 32
2178309
(0.00 secs, 0 bytes)