在Haskell中,这是一个简单(天真)定义的定义
fix :: (a -> a) -> a
fix f = f (fix f)
但是,Haskell实际上是如何实现它的(效率更高)
fix f = let x = f x in x
我的问题是为什么第二个比第一个更有效?
答案 0 :(得分:22)
缓慢的fix
在递归的每一步调用f
,而快速的f
只调用import Debug.Trace
fix f = f (fix f)
fix' f = let x = f x in x
facf :: (Int -> Int) -> Int -> Int
facf f 0 = 1
facf f n = n * f (n - 1)
tracedFacf x = trace "called" facf x
fac = fix tracedFacf
fac' = fix' tracedFacf
一次。它可以通过跟踪来显示:
> fac 3
called
called
called
called
6
> fac' 3
called
6
现在尝试一些运行:
let x = f x in x
更详细地说,x
会导致为f
分配一个懒惰的thunk,并将指向此thunk的指针传递给fix' f
。在首次评估f
时,将评估thunk,并且对它的所有引用(此处具体为:我们传递给x
的那个)将重定向到结果值。只是x
被赋予了一个值,该值还包含对{{1}}的引用。
我承认这可能是令人费解的。这是在懒惰工作时应该习惯的东西。
答案 1 :(得分:9)
当你用一个带有两个参数的函数调用fix
以产生一个带一个参数的函数时,我不认为这总是(或必然)有所帮助。你必须运行一些基准才能看到。但你也可以用一个参数来调用它!
fix (1 :)
是循环链表。使用fix
的幼稚定义,它将是一个无限的列表,随着结构的强制,新的部分会被懒散地构建。
答案 2 :(得分:5)
我相信已经有人问过,但我找不到答案。原因是第一个版本
fix f = f (fix f)
是递归函数,因此无法内联然后进行优化。来自GHC manual:
例如,对于自递归函数,循环断开器只能是函数本身,因此始终忽略
INLINE
编译指示。
但是
fix f = let x = f x in x
不是递归的,递归被移入let
绑定,因此可以内联它。
更新:我做了一些测试,而前一版本没有内联,而后者确实没有内联,但它对性能似乎并不重要。所以其他解释(堆上的单个对象与每次迭代创建一个)似乎更准确。