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


87

我对文档感到有些困惑fix(尽管我认为我现在知道它应该做什么),所以我查看了源代码。这让我更加困惑:

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

这究竟如何返回固定点?

我决定在命令行中尝试一下:

Prelude Data.Function> fix id
...

它挂在那里。现在说句公道话,这是在我的旧Macbook上,有点慢。但是,此函数在计算上不会昂贵,因为传递给id的任何东西都会返回相同的结果(更不用说它占用了CPU时间)。我究竟做错了什么?


68
恶作剧的答案是:“修复程序没有真正的用途,它就在那里,因此您可以输入fix errorghci,让自己感觉良好。”
Thomas M. DuBuisson

3
@TomMD-好笑。我会记住,如果有人问我该怎么做,我感觉很烦。:-)
Jason Baker

2
我通常写fixfix f = f (fix f)。简短,简单,有效并且与数学定义相同。
newacct 2011年

20
@newacct,是的,我也是这么想的。但是这里的方法可以导致更有效的结构。如果评估,您可以看到差异fix (1:) !! (10^8)。原来是在恒定内存中完成的,而您则需要线性内存(这也使它变慢了很多)。也就是说,使用let“打得更紧”,并允许生成圆形数据结构,而您的则不会。
luqui 2011年

22
您也可以重新发明fix帮助我了解fix了很多。
fredoverflow 2011年

Answers:


90

你什么都没做错。 fix id是一个无限循环。

当我们说fix返回函数的最小不动点时,我们指的是领域理论上的意义。所以fix (\x -> 2*x-1)不是要回1,因为虽然1是功能的一个固定点,它是不是至少一个在域排序。

我无法仅用一两段时间来描述领域顺序,因此,我将向您介绍上面的领域理论链接。这是一个很棒的教程,易于阅读,并且很有启发性。我强烈推荐它。

对于10,000英尺的视图来说,fix是一个高阶函数,它编码了递归的概念。如果您有表达式:

let x = 1:x in x

这就产生了无限列表[1,1..],您可以使用fix以下方法说同样的话:

fix (\x -> 1:x)

(或简单地fix (1:)),它表示找到(1:)函数的固定点,IOW一个值,xx = 1:x...就像我们上面定义的那样。从定义中可以看到,fix仅此想法而已-递归封装到一个函数中。

这也是一个真正的通用递归概念-您可以用这种方式编写任何递归函数,包括使用多态递归的函数。因此,例如典型的斐波那契函数:

fib n = if n < 2 then n else fib (n-1) + fib (n-2)

可以这样写fix

fib = fix (\f -> \n -> if n < 2 then n else f (n-1) + f (n-2))

练习:扩展的定义fix以显示这两个定义fib是等效的。

但是要获得全面的了解,请阅读有关领域理论的知识。这真的是很酷的东西。


32
这是一种相关的思考方式fix idfix接受type函数a -> a并返回type值a。因为id对any都是多态的a,所以fix id将具有type a,即任何可能的值。在Haskell中,唯一可以是任何类型的值是bottom,⊥,并且与非终止计算是无法区分的。因此,fix id产生的值恰好是底值。的危险fix是,如果⊥是函数的动点,那么根据定义,它是最小不动点,因此fix不会终止。
约翰L

4
Haskell中的@JohnLundefined也是任何类型的值。您可以定义fix为:fix f = foldr (\_ -> f) undefined (repeat undefined)
didest 2011年

1
@Diego您的代码等同于_Y f = f (_Y f)
内斯

25

我并不声称完全理解这一点,但是如果这可以帮助任何人……那么yippee。

考虑的定义fixfix f = let x = f x in x。令人难以置信的部分x是定义为f x。但是想一分钟。

x = f x

由于x = fx,那么我们可以用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)是。所以我们可以这么说。这样一来,fix将采用我们的函数,即或实际上是,并将返回type的结果,换句话说,(b -> b) -> b -> b(b -> b) -> (b -> b)a = (b -> b)a -> a(b -> b) -> (b -> b)ab -> b换句话说,其他功能!

等等,我以为应该返回一个固定点,而不是一个函数。很好(因为函数是数据)。您可以想象,它为我们提供了确定阶乘的确定函数。我们给了它一个不知道如何递归的函数(因此它的参数之一就是用来递归的函数),并fix教了它如何递归。

还记得我曾说过f必须生成某种结构以便以后f进行模式匹配和终止的说法吗?好吧,那不完全正确,我猜。TomMD说明了我们如何扩展x以应用该功能并逐步迈向基本案例。对于他的功能,他使用了if / then,这就是导致终止的原因。在重复替换之后,in整个定义的一部分fix最终将停止根据进行定义x,也就是说,该定义是可计算的和完整的。


谢谢。这是一个非常有用和实用的解释。
kizzx2 2011年

17

您需要一种方法来终止该固定点。扩展您的示例很显然,它不会完成:

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 -->

我会在这里停止!


您的回答fix对我而言实际上是有意义的。我的回答很大程度上取决于您已经说过的话。
丹·伯顿

@托马斯,您的两种还原顺序均不正确。:)id x减小到x(然后减小到id x)。-然后,在第二个样本(fact)中,当x第一次强制应用thunk时,将记住并重新使用结果值。(\recurse ...) x使用非共享定义y g = g (y g)而不是使用此共享fix定义将重新计算。-我已经在这里进行了试用编辑-欢迎使用它,或者如果您批准的话,我可以进行编辑。
内斯

实际上,当fix id减小时,let x = id x in x也会id xlet框架内强制应用程序的值(thunk),因此将其减小到let x = x in x,然后循环。看起来像它。
尼斯(Ness Ness)2014年

正确。我的答案是使用方程式推理。表示减少量的la Haskell与评估顺序有关,这只会使示例混淆,而没有任何真正的收获。
Thomas M. DuBuisson 2014年

1
该问题同时带有haskell和letrec标记(即,具有共享的递归let)。在Haskell中fixYY之间的区别非常明显且很重要。当正确的缩短顺序更短,更清晰,更容易遵循并正确反映实际情况时,我看不出显示错误的减少顺序有什么好处。
Will Ness 2014年

3

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

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

如您所见,undefined是一个固定点,因此fix将其选中。如果您改为(\ x-> 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,没有无限循环或未定义。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.