我是haskell的初学者并试图理解Let vs Where wiki page。最后有一个例子,在函数定义x
的左侧添加参数fib
会改变语义。
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
维基页面指出“在第二种情况下[fib2
],为每个参数x”重新定义了fib'。我正在寻找一个初学者友好的解释为什么会发生这种情况,一般来说,是否有更多这样隐藏的副作用?
维基页面还有一个指向eta reduction的解释的链接,其中指出表达式及其eta减少是等效的。那么,如果fib2
是fib1
的eta抽象,为什么它们不等同?
答案 0 :(得分:5)
您的最后一点可能是最重要的一点 - fib1
和fib2
确实是等效的,编译器可以完全自由地将一个转换为另一个。由于这种转换对指称语义没有影响,只有操作语义,如果它“改进”程序的操作语义,它通常被称为优化。麻烦的是,很难说出“改进”的真正含义。
维基页面说明
编译器无法知道您是否打算这样做 - 而它 增加时间复杂度可能会降低空间复杂性。
这是事实 - 一般来说,这种“优化”可能不是优化,具体取决于您如何衡量绩效。因此,一般情况下,编译器不会执行此优化,如果他们愿意,请将这样做的负担留给程序员,除非它确实确定它确实会提高代码的性能。
差异是一种称为共享的特征。在fib1
中,fib'
中的map fib'
来电将在fib1
功能内fib'
的不同来电之间共享。这意味着只要计算函数,就必须保留整个列表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' ..
。
因此它不会将定义从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)