为什么这个版本的'fix'在Haskell中更有效?

时间:2016-05-21 17:45:23

标签: haskell

在Haskell中,这是一个简单(天真)定义的定义

fix :: (a -> a) -> a
fix f = f (fix f)

但是,Haskell实际上是如何实现它的(效率更高)

fix f = let x = f x in x

我的问题是为什么第二个比第一个更有效?

3 个答案:

答案 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绑定,因此可以内联它。

更新:我做了一些测试,而前一版本没有内联,而后者确实没有内联,但它对性能似乎并不重要。所以其他解释(堆上的单个对象与每次迭代创建一个)似乎更准确。

相关问题