F#递归对象

时间:2018-02-02 12:00:14

标签: recursion f#

我是F#和功能语言的新手。所以这可能是一个愚蠢的问题,或者与Recursive objects in F#?重复,但我不知道。

这是一个简单的Fibonacci函数:

let rec fib n = 
    match n with
    | 0 -> 1
    | 1 -> 1
    | _ -> fib (n - 1) + fib (n - 2)

其签名为int -> int

可以改写为:

let rec fib = 
    fun n ->
        match n with
        | 0 -> 1
        | 1 -> 1
        | _ -> fib (n - 1) + fib (n - 2)

它的签名是(int -> int)(在Visual Studio for Mac中)。

那与前一个有什么不同?

如果我再添加一行:

let rec fib = 
    printfn "fib" // <-- this line
    fun n ->
        match n with
        | 0 -> 1
        | 1 -> 1
        | _ -> fib (n - 1) + fib (n - 2)

IDE给了我一个警告:

  

警告FS0040:通过使用延迟引用,将在运行时检查对定义的对象的此递归引用和其他递归引用的初始化 - 健全性。这是因为您定义了一个或多个递归对象,而不是递归函数。使用'#nowarn“40”'或'--nowarn:40'可以抑制此警告。

此行如何影响初始化?

“递归对象”是什么意思?我在文档中找不到它。

更新

感谢您的回复,非常好的解释。

在阅读完答案后,我对递归对象有一些想法。

首先,我在签名上犯了一个错误。上面的前两个代码段具有相同的签名int -> int;但是最后一个有签名(int -> int)(注意:签名在vscode中具有不同的表示形式,具有Ionide扩展名)。

我认为两个签名之间的区别在于,第一个意味着它只是一个函数,另一个意味着它是对函数的引用,即对象

每个没有let rec something的{​​{1}}是一个对象而不是一个函数,请参见函数definition,而第二个片段是一个异常,可能由编译器优化为函数

一个例子:

parameter-list

我能想到的唯一一个原因是编译器不够智能,它会抛出一个警告,因为它是一个递归对象,就像警告所示,

  

这是因为您定义了一个或多个递归对象,而不是递归函数

即使这种模式永远不会有任何问题。

let rec x = (fun () -> x + 1)() // same warning, says `x` is an recursive object

您对此有何看法?

2 个答案:

答案 0 :(得分:5)

“递归对象”就像递归函数一样,除了它们是对象。不是功能。

递归函数是一个引用自身的函数,例如:

let rec f x = f (x-1) + 1

递归对象类似,因为它引用自身,除了它不是函数,例如:

let rec x = x + 1

以上实际上不会编译。 F#编译器能够正确地确定问题并发出错误:The value 'x' will be evaluated as part of its own definition。显然,这样的定义是荒谬的:为了计算x,您需要知道x。不计算。

但是,让我们看看我们是否可以更聪明。如果我在lambda表达式中关闭x怎么样?

let rec x = (fun() -> x + 1) ()

在这里,我将x包装在一个函数中,并立即调用该函数。这会编译,但会发出警告 - 与您相同的警告,“在运行时检查初始化 - 健全性”。

让我们去运行时:

> let rec x = (fun() -> x + 1) ()
System.InvalidOperationException: ValueFactory attempted to access the Value property of this instance.

毫不奇怪,我们收到了一个错误:事实证明,在此定义中,需要了解x才能计算x - 与{let rec x = x + 1相同1}}。

但如果是这样的话,为什么要编译呢?嗯,事实上,通常情况下,不可能严格证明x在初始化期间将会或不会访问自己。编译器足够聪明,可以注意到可能发生(这就是它发出警告的原因),但不足以证明肯定会发生。

所以在这种情况下,除了发出警告之外,编译器还会安装一个运行时保护程序,它将检查x在被访问时是否已经初始化。带有这种警卫的编译代码可能如下所示:

let mutable x_initialized = false
let rec x = 
    let x_temp = 
        (fun() -> 
            if not x_initialized then failwith "Not good!"
            else x + 1
        ) ()
    x_initialized <- true
    x_temp

实际的编译代码当然看起来不一样;使用ILSpy看看你是否好奇)

在某些特殊情况下,编译器可以证明这种或那种方式。在其他情况下它不能,所以它安装运行时保护:

// Definitely bad => compile-time error
let rec x = x + 1

// Definitely good => no errors, no warnings
let rec x = fun() -> x() + 1

// Might be bad => compile-time warning + runtime guard
let rec x = (fun() -> x+1) ()

// Also might be bad: no way to tell what the `printfn` call will do
let rec x = 
    printfn "a"
    fun() -> x() + 1

答案 1 :(得分:3)

最后两个版本之间存在重大差异。请注意,向第一个版本添加printfn调用不会产生任何警告,并且每次函数递归时都会打印

"fib"

let rec fib n = printfn "fib" match n with | 0 -> 1 | 1 -> 1 | _ -> fib (n - 1) + fib (n - 2) > fib 10;; fib fib fib ... val it : int = 89 调用是递归函数体的一部分。但是,当定义函数时,第3个/最终版本只打印printfn 一次,然后再也不会。

有什么区别?在第3版中,您不是仅定义递归函数,因为还有其他表达式在lambda上创建闭包,从而产生递归对象。考虑这个版本:

"fib"

let rec fib3 = let x = 1 let y = 2 fun n -> match n with | 0 -> x | 1 -> x | _ -> fib3 (n - x) + fib3 (n - y) 不是一个普通的递归函数;捕获fib3x的函数有一个闭包(对y版本也是如此,尽管它只是一个副作用)。这个闭包是警告中提到的“递归对象”。每次递归都不会重新定义printfnx;它们是根级闭包/递归对象的一部分。

来自链接的问题/答案:

  

因为[编译器]不能保证在初始化之前不会访问引用

虽然它不适用于您的特定示例,但编译器无法知道您在y之前是否正在做无害的事情,或者可能在fib3定义中引用/调用lambda值/已初始化。 Here's another good answer explaining the same.