15

查看 GHC 源代码,我可以看到修复的定义是:

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

在一个示例中,修复是这样使用的:

fix (\f x -> let x' = x+1 in x:f x')

这基本上会产生一个以 1 增加到无穷大的数字序列。为了发生这种情况,修复必须将它接收到的函数作为第一个参数返回到该函数。我不清楚上面列出的修复定义如何做到这一点。

这个定义是我理解修复如何工作的方式:

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

所以现在我有两个问题:

  1. 在第一个定义中,x是如何表示修复 x的?
  2. 使用第一个定义比使用第二个定义有什么优势吗?
4

4 回答 4

16

通过应用等式推理,很容易看出这个定义是如何工作的。

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

x当我们尝试评估时会评估fix f什么?它定义为f x,所以fix f = f x。但是x这里有什么?是的f x,和以前一样。所以你得到fix f = f x = f (f x). f以这种方式推理,您将获得: fix f=的无限应用链f (f (f (f ...)))

现在,代替(\f x -> let x' = x+1 in x:f x')f得到

fix (\f x -> let x' = x+1 in x:f x')
    = (\f x -> let x' = x+1 in x:f x') (f ...)
    = (\x -> let x' = x+1 in x:((f ...) x'))
    = (\x -> x:((f ...) x + 1))
    = (\x -> x:((\x -> let x' = x+1 in x:(f ...) x') x + 1))
    = (\x -> x:((\x -> x:(f ...) x + 1) x + 1))
    = (\x -> x:(x + 1):((f ...) x + 1))
    = ...

编辑:关于你的第二个问题,@is7s 在评论中指出第一个定义更可取,因为它更有效。

要找出原因,让我们看看核心fix1 (:1) !! 10^8

a_r1Ko :: Type.Integer    
a_r1Ko = __integer 1

main_x :: [Type.Integer]   
main_x =
  : @ Type.Integer a_r1Ko main_x

main3 :: Type.Integer
main3 =
  !!_sub @ Type.Integer main_x 100000000

如您所见,转换后fix1 (1:)基本上变成了main_x = 1 : main_x. 请注意这个定义是如何指代自己的——这就是“打结”的意思。这种自引用在运行时表示为简单的指针间接:

修复1

现在让我们看看fix2 (1:) !! 100000000

main6 :: Type.Integer
main6 = __integer 1

main5
  :: [Type.Integer] -> [Type.Integer]
main5 = : @ Type.Integer main6

main4 :: [Type.Integer]
main4 = fix2 @ [Type.Integer] main5

main3 :: Type.Integer
main3 =
  !!_sub @ Type.Integer main4 100000000

这里fix2实际上保留了应用程序:

修复2

结果是第二个程序需要为列表的每个元素进行分配(但由于列表立即被消耗,程序仍然有效地在恒定空间中运行):

$ ./Test2 +RTS -s
   2,400,047,200 bytes allocated in the heap
         133,012 bytes copied during GC
          27,040 bytes maximum residency (1 sample(s))
          17,688 bytes maximum slop
               1 MB total memory in use (0 MB lost due to fragmentation)
 [...]

将其与第一个程序的行为进行比较:

$ ./Test1 +RTS -s          
          47,168 bytes allocated in the heap
           1,756 bytes copied during GC
          42,632 bytes maximum residency (1 sample(s))
          18,808 bytes maximum slop
               1 MB total memory in use (0 MB lost due to fragmentation)
[...]
于 2012-10-21T16:01:59.990 回答
9

在第一个定义中,x 是如何表示修复 x 的?

fix f = let x = f x in x

让 Haskell 中的绑定是递归的

首先,意识到 Haskell 允许递归 let 绑定。Haskell 称之为“let”,其他一些语言称之为“letrec”。这对于函数定义来说感觉很正常。例如:

ghci> let fac n = if n == 0 then 1 else n * fac (n - 1) in fac 5
120

但是对于值定义来说,这似乎很奇怪。尽管如此,由于 Haskell 的非严格性,值可以递归定义。

ghci> take 5 (let ones = 1 : ones in ones)
[1,1,1,1,1]

有关 Haskell 惰性的更多详细信息,请参阅第 3.3 和 3.4 节对 Haskell 的简要介绍。

GHC 中的重击

在 GHC 中,一个尚未计算的表达式被包裹在一个“thunk”中:一个执行计算的承诺。只有在绝对必要时才会评估 Thunks 。假设我们想要fix someFunction。根据 的定义fix,即

let x = someFunction x in x

现在,GHC 看到的是这样的东西。

let x = MAKE A THUNK in x

所以它很高兴地为你发出一声砰的一声,然后继续前进,直到你要求知道x实际上是什么。

样品评估

thunk 的表达式恰好指的是它自己。让我们ones举个例子并重写它以使用fix.

ghci> take 5 (let ones recur = 1 : recur in fix ones)
[1,1,1,1,1]

那么这个thunk会是什么样子呢?
我们可以将内联ones作为匿名函数\recur -> 1 : recur进行更清晰的演示。

take 5 (fix (\recur -> 1 : recur))

-- expand definition of fix
take 5 (let x = (\recur -> 1 : recur) x in x)

那么,什么 x?好吧,即使我们不太确定是什么x,我们仍然可以使用函数应用程序:

take 5 (let x = 1 : x in x)

嘿,看,我们又回到了之前的定义。

take 5 (let ones = 1 : ones in ones)

因此,如果您相信自己了解它的工作原理,那么您就会很好地了解它的fix工作原理。


使用第一个定义比使用第二个定义有什么优势吗?

是的。问题是第二个版本可能会导致空间泄漏,即使进行了优化。有关. _ _ forever这就是我提到 thunk 的原因:因为let x = f x in x只分配一个 thunk:xthunk。

于 2012-10-22T04:36:33.687 回答
5

区别在于共享与复制。1

fix1 f = x where x = f x    -- more visually apparent way to write the same thing

fix2 f = f (fix2 f)

如果我们将定义代入其自身,则两者都被简化为相同的无限应用链 f (f (f (f (f ...))))。但是第一个定义使用显式命名;在 Haskell 中(就像在大多数其他语言中一样),共享是通过命名事物的能力实现的:一个名称或多或少地保证引用一个“实体”(此处为x)。第二个定义不保证任何共享——调用的结果fix2 f被替换到表达式中,所以它也可以被替换为一个值。

但是理论上,给定的编译器可能对此很聪明,并且在第二种情况下也可以使用共享。

相关问题是“ Y组合器”。在没有命名构造(因此没有引用)的无类型 lambda 演算中, Y组合器通过安排要复制的定义来模拟自引用,因此引用 self 的副本成为可能。但是在使用环境模型允许语言中的命名实体的实现中,通过名称直接引用成为可能。

要查看两个定义之间更显着的差异,请比较

fibs1 = fix1 ( (0:) . (1:) . g ) where g (a:t@(b:_)) = (a+b):g t
fibs2 = fix2 ( (0:) . (1:) . g ) where g (a:t@(b:_)) = (a+b):g t

也可以看看:

(特别是尝试找出上面最后一个链接中的最后两个定义)。


1根据定义工作,对于您的示例,fix (\g x -> let x2 = x+1 in x : g x2)我们得到

fix1 (\g x -> let x2 = x+1 in x : g x2)
 = fix1 (\g x -> x : g (x+1))
 = fix1 f where {f = \g x -> x : g (x+1)}
 = fix1 f where {f g x = x : g (x+1)}
 = x      where {x = f x ; f g x = x : g (x+1)}
 = g      where {g = f g ; f g x = x : g (x+1)}   -- both g in {g = f g} are the same g
 = g      where {g = \x -> x : g (x+1)}           -- and so, here as well
 = g      where {g x = x : g (x+1)}

因此g实际上创建了一个适当的递归定义。(在上面,我们写....x.... where {x = ...}let {x = ...} in ....x...., 是为了便于阅读)。

但是第二个推导进行了一个关键的区别,即用一个值代替一个,而不是一个名称,因为

fix2 (\g x -> x : g (x+1))
 = fix2 f             where {f g x = x : g (x+1)}
 = f (fix2 f)         where {f g x = x : g (x+1)}
 = (\x-> x : g (x+1)) where {g = fix2 f ; f g x = x : g (x+1)}
 = h                  where {h   x = x : g (x+1) ; g = fix2 f   ; f g x = x : g (x+1)}

所以实际的通话将继续进行,例如

take 3 $ fix2 (\g x -> x : g (x+1)) 10
 = take 3 (h 10)      where {h   x = x : g (x+1) ; g = fix2 f   ; f g x = x : g (x+1)}
 = take 3 (x:g (x+1)) where {x = 10 ;              g = fix2 f   ; f g x = x : g (x+1)}
 = x:take 2 (g x2)    where {x2 = x+1 ; x = 10 ;   g = fix2 f   ; f g x = x : g (x+1)}
 = x:take 2 (g x2)    where {x2 = x+1 ; x = 10 ; g = f (fix2 f) ; f g x = x : g (x+1)}
 = x:take 2 (x2 : g2 (x2+1))   where {             g2 = fix2 f  ;
                             x2 = x+1 ; x = 10 ;                  f g x = x : g (x+1)}
 = ......

我们看到这里建立了一个的绑定(for g2),而不是像定义g一样重用之前的绑定(for)。fix1

于 2012-10-23T11:33:52.837 回答
5

我可能有一些来自内联优化的简化解释。如果我们有

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

thenfix是一个递归函数,这意味着它不能在使用它的地方内联(INLINE如果给出,编译指示将被忽略)。

然而

fix' f = let x = f x in x

不是递归函数 - 它从不调用自身只有x内部是递归的。所以打电话的时候

fix' (\r x -> let x' = x+1 in x:r x')

编译器可以将其内联到

(\f -> (let y = f y in y)) (\r x -> let x' = x+1 in x:r x')

然后继续简化它,例如

let y = (\r x -> let x' = x+1 in x:r x') y in y 
let y = (\  x -> let x' = x+1 in x:y x')   in y 

这就像函数是使用标准递归表示法定义的,但没有fix

    y       x =  let x' = x+1 in x:y x'   
于 2012-10-28T06:32:39.990 回答