为什么在F#中使用递归函数而不是`while true do`?

时间:2018-04-30 13:13:18

标签: recursion f# functional-programming

在观看Tomas Petricek的Pluralsight course时(我假设他知道他在说什么),我看到的代码如下......

let echo =
  MailboxProcessor<string>.Start(fun inbox ->
    async {
      while do true
        let! msg = inbox.Receive()
    printfn "Hello %s" msg
    })

忽略这是演示代理的事实,我对内部函数感兴趣,它使用while do true来无限期地运行它。

在寻找代理商的其他例子的同时,我看到很多其他人都使用这样的代码......

let counter =
  MailboxProcessor.Start(fun inbox ->
    let rec loop n =
      async { do printfn "n = %d, waiting..." n
        let! msg = inbox.Receive()
        return! loop(n+msg) }
    loop 0)

Wikibooks复制的代码。

这里的内部函数是递归的,并且在主函数声明结束之前通过使用基值调用它来启动。

现在我意识到在第二种情况下,递归是一种将私有值传递给内部函数而不必使用可变局部值的方便方法,但是在这里使用递归而不是{{1}还有其他任何理由}?使用递归编写第一个代码片段会有什么好处吗?

我发现非递归版本更容易阅读(当然是主观意见),这似乎是尽可能使用它的一个很好的理由。

3 个答案:

答案 0 :(得分:14)

具体谈到MailboxProcessor,我认为选择取决于你究竟在做什么。通常,您始终可以使用while循环或递归。

递归使得更容易使用不可变状态,如果你没有状态或者你使用可变状态,我发现while循环更好。使用可变状态通常非常有用,因为MailboxProcessor可以保护您免受并发访问,并且您可以将状态保持为本地状态,因此Dictionary(高效哈希表)之类的东西通常很有用。

一般来说:

  • 如果您不需要任何州,我希望while
  • 如果您有可变状态(例如DictionaryResizeArray),我会选择while
  • 如果你有一些不可变状态(比如功能列表或整数),那么递归就更好了
  • 如果您的逻辑在多种操作模式之间切换,那么您可以将其编写为两个相互递归的函数,这对于循环来说是不可行的。

答案 1 :(得分:3)

在许多情况下,这取决于您希望如何编码。就像你的例子。 您可以编写recursiv的所有内容,您也可以使用循环编写,但有时像递归数据结构一样,以递归样式编写更容易。 在大学时我了解到,通过递归编程,你只需要看下一步非常方便!

您可能对此问题感兴趣,因为它更进一步解释了我的答案: recursion versus iteration

答案 2 :(得分:1)

在F#forwhile循环中,表达式缺乏其他语言中常见的功能:

  1. continue - 跳过循环的其余部分,然后从循环表达式的顶部重新开始。
  2. break - 过早停止循环。
  3. 如果您希望continuebreak不做我在开头做的事情,请为while循环编写一个非常复杂的测试表达式。相反,尾递归是F#中的最佳答案:

    let vs : int [] = ...
    let rec findPositiveNumberIndex i =
      if i < vs.Length then
        if vs.[i] > 0 then 
          Some i
        else 
          findPositiveNumberIndex (i + 1)
      else
        None
    match findPositiveNumberIndex 0 with
    | Some i -> printfn "First positive number index: %d" i
    | None   -> printfn "No positive numbers found"
    

    在这样的代码中,F#应用了一些调用尾调用优化(TCO),它将上面的代码转换为while循环break。这意味着我们不会耗尽堆栈空间并且循环是有效的。 TCO是C#缺少的一个功能,所以我们不想在C#中编写如上所示的代码。

    就像其他人说尾递归一样,你有时可以避免可变状态但不是全部。

    使用尾递归,循环表达式可以返回一个很好的结果。

    此外,如果您希望在F#中快速迭代int64之类的类型,或者使用除1-1以外的增量,则必须依赖尾递归。原因是F#仅对for1的整数和增量执行有效的-1表达式。

    for i in 0..100 do
      printfn "This is a fast loop"
    
    for i in 0..2..100 do
      printfn "This is a slow loop"  
    
    for i in 0L..100L do
      printfn "This is a slow loop"    
    

    有时候在寻找性能时,一个技巧是循环到0(保存CPU寄存器)。不幸的是,F#生成for循环代码的方式并不像人们希望的那样好:

    for i = 100 downto 0 do
      printfn "Unfortunately this is not as efficient as it can be"
    

    0的尾递归保存了CPU寄存器。

    (不幸的是,F#编译器没有合并测试和尾递归的循环指令,所以它不是那么好)