我如何使用修复程序,它是如何工作的?

时间:2011-01-24 21:34:16

标签: haskell y-combinator letrec

我对fix的文档感到有点困惑(虽然我想我明白它应该做什么),所以我查看了源代码。这让我更加困惑:

fix :: (a -> a) -> a
fix f = let x = f x in x

这究竟是如何返回固定点的?

我决定在命令行试试:

Prelude Data.Function> fix id
...

它挂在那里。现在公平地说,这是在我的旧Macbook上,这有点慢。但是,这个函数不能计算上昂贵,因为传入id的任何东西都会返回相同的东西(更不用说它不占用CPU时间)。我做错了什么?

5 个答案:

答案 0 :(得分:81)

你没有做错任何事。 fix id是一个无限循环。

当我们说fix返回函数的最小固定点时,我们的意思是domain theory意义上的。fix (\x -> 2*x-1)。所以1 将返回1,因为尽管fix是该函数的固定点,但它不是最少域名订购中的一个。

我无法在一两段内描述域名排序,因此我将向您推荐上面的域名理论链接。这是一个很好的教程,易于阅读,而且非常有启发性。我强烈推荐它。

对于10,000英尺的视图,let x = 1:x in x 是一个高阶函数,它编码递归的概念。如果您有表达式:

[1,1..]

哪个会产生无限列表fix,您可以使用fix (\x -> 1:x) 说同样的事情:

fix (1:)

(或简称为(1:)),它说我找到x函数的固定点,Iow值为x = 1:xfix ...就像我们一样定义如上。从定义中可以看出,fib n = if n < 2 then n else fib (n-1) + fib (n-2) 只不过是这个想法 - 将递归封装到一个函数中。

这也是一个真正普遍的递归概念 - 你可以用这种方式编写任何递归函数,including functions that use polymorphic recursion。例如,典型的斐波纳契函数:

fix

可以用fib = fix (\f -> \n -> if n < 2 then n else f (n-1) + f (n-2)) 这样写:

fix

练习:扩展fib的定义,以显示{{1}}的这两个定义是等效的。

但是要完全理解,请阅读有关领域理论的内容。这真的很酷。

答案 1 :(得分:20)

我根本不会理解这一点,但如果这有助于任何人......那么yippee。

考虑fix的定义。 fix f = let x = f x in x。令人难以置信的部分是x被定义为f x。但请考虑一下。

x = f x

由于x = f x,那么我们可以用右边的x替换它,对吧?因此......

x = f . f $ x -- or x = f (f x)
x = f . f . f $ x -- or x = f (f (f x))
x = f . f . f . f . f . f . f . f . f . f . f $ x -- etc.

所以诀窍是,为了终止,f必须生成某种结构,以便后来的f可以模式匹配该结构并终止递归,而不必实际关注其参数的完整“值”(?)

当然,除非你想做一些创建无限列表的事情,如luqui所示。

TomMD的因子解释很好。 Fix的类型签名是(a -> a) -> a(\recurse d -> if d > 0 then d * (recurse (d-1)) else 1)的类型签名为(b -> b) -> b -> b,换句话说,(b -> b) -> (b -> b)。所以我们可以说a = (b -> b)。这样,修复就会使用我们的函数a -> a,或者真的(b -> b) -> (b -> b),并返回a类型的结果,换句话说,b -> b,其他单词,另一个功能!

等等,我以为它应该返回一个固定的点...而不是一个函数。它确实如此(因为函数是数据)。你可以想象它给了我们找到阶乘的决定性功能。我们给了它一个不知道如何递归的函数(因此其中一个参数是用于递归的函数),fix教它如何递归。

还记得我是怎么说f必须生成某种结构,以便后来的f可以模式匹配并终止吗?嗯,这不完全正确,我想。 TomMD说明了我们如何扩展x以应用函数和基础案例的步骤。对于他的功能,他使用了if / then,这就是导致终止的原因。在重复替换之后,in的整个定义的fix部分最终会停止以x的形式定义,即可以计算和完成。

答案 2 :(得分:15)

您需要一种方法让fixpoint终止。扩展你的例子显然它不会完成:

fix id
--> let x = id x in x
--> id x
--> id (id x)
--> id (id (id x))
--> ...

这是我使用修复程序的一个真实示例(请注意,我不经常使用修复程序,并且在编写此代码时可能已经厌倦/不担心可读代码):

(fix (\f h -> if (pred h) then f (mutate h) else h)) q

WTF,你说!嗯,是的,但这里有一些非常有用的要点。首先,你的第一个fix参数通常应该是一个“递归”情况的函数,第二个参数是要作用的数据。这是与命名函数相同的代码:

getQ h
      | pred h = getQ (mutate h)
      | otherwise = h

如果你仍然感到困惑,那么也许因子将是一个更简单的例子:

fix (\recurse d -> if d > 0 then d * (recurse (d-1)) else 1) 5 -->* 120

注意评估:

fix (\recurse d -> if d > 0 then d * (recurse (d-1)) else 1) 3 -->
let x = (\recurse d -> if d > 0 then d * (recurse (d-1)) else 1) x in x 3 -->
let x = ... in (\recurse d -> if d > 0 then d * (recurse (d-1)) else 1) x 3 -->
let x = ... in (\d -> if d > 0 then d * (x (d-1)) else 1) 3
哦,你看到了吗? x成为我们then分支内的一个函数。

let x = ... in if 3 > 0 then 3 * (x (3 - 1)) else 1) -->
let x = ... in 3 * x 2 -->
let x = ... in 3 * (\recurse d -> if d > 0 then d * (recurse (d-1)) else 1) x 2 -->

在上文中,您需要记住x = f x,因此最后x 2的两个参数而不仅仅是2

let x = ... in 3 * (\d -> if d > 0 then d * (x (d-1)) else 1) 2 -->

我会在这里停下来!

答案 3 :(得分:9)

一个明确的定义是You could have re-invented fix too!

答案 4 :(得分:2)

我理解的是,它找到了函数的值,这样它就会输出你给它的东西。问题是,它将始终选择未定义(或无限循环,在haskell中,未定义和无限循环是相同的)或其中包含最多未定义的内容。例如,使用id,

λ <*Main Data.Function>: id undefined
*** Exception: Prelude.undefined

如您所见,undefined是一个固定点,因此fix会选择它。如果您改为(\ x-&gt; 1:x)。

λ <*Main Data.Function>: undefined
*** Exception: Prelude.undefined
λ <*Main Data.Function>: (\x->1:x) undefined
[1*** Exception: Prelude.undefined

所以fix无法选择未定义的。为了使它更多地连接到无限循环。

λ <*Main Data.Function>: let y=y in y
^CInterrupted.
λ <*Main Data.Function>: (\x->1:x) (let y=y in y)
[1^CInterrupted.

再次,略有不同。那么固定点是什么?我们试试repeat 1

λ <*Main Data.Function>: repeat 1
[1,1,1,1,1,1, and so on
λ <*Main Data.Function>: (\x->1:x) $ repeat 1
[1,1,1,1,1,1, and so on

它是一样的!由于这是唯一的固定点,fix必须解决它。抱歉fix,没有无限循环或未定义。