【问题标题】:While or Tail Recursion in F#, what to use when?F#中的while或尾递归,什么时候使用?
【发布时间】:2010-12-20 07:32:17
【问题描述】:

好的,仅在 F# 中,这就是我现在的理解:

  • 有些问题本质上是递归的(构建或读出树结构以仅举一个例子),然后使用递归。在这些情况下,您最好使用尾递归来中断堆栈

  • 一些语言是纯函数式的,所以你必须使用递归而不是 while 循环,即使问题本质上不是递归的

所以我的问题是:由于 F# 也支持命令式范式,您会在 F# 中使用尾递归来解决自然递归的问题吗?特别是因为我已经阅读了编译器 recongnizes tail recursion 并只是在 while 循环中转换它?

如果是这样:为什么?

【问题讨论】:

标签: f# functional-programming while-loop tail-recursion


【解决方案1】:

最好的答案是“都不是”。 :)

while 循环和尾递归都有一些丑陋之处。

虽然循环需要可变性和效果,尽管我并不反对适度使用这些,尤其是在封装在本地函数的上下文中时,但当你开始引入效果时,你有时会觉得你的程序混乱/丑化了只是循环。

尾递归通常具有需要额外的累加器参数或继续传递样式的缺点。这会使程序杂乱无章地使用额外的样板来处理函数的启动条件。​​

最好的答案是既不使用 while 循环也不使用递归。高阶函数和 lambda 是你的救星,尤其是映射和折叠。当您可以将这些控制结构封装在可重用的库中,然后简单地以声明的方式陈述您的计算的本质时,为什么还要胡乱使用凌乱的循环控制结构呢?

如果您养成了经常调用 map/fold 而不是使用循环/递归的习惯,并且在您引入的任何新的树形结构数据类型中提供了 fold 函数,那么您将走得更远。 :)

对于那些有兴趣了解 F# 中折叠的更多信息的人,何不查看我的 first three blog 关于该主题的系列帖子?

【讨论】:

  • 那么,当您掌握 f#(我假设您会这样做)时,是否可以在很大程度上避免递归和 while 循环?甚至总是?
  • 是的;更一般地说,当您掌握任何函数式编程语言以及如何使用 lambda 时,您将编写更少的显式循环/递归,因为您将利用更多为您执行循环的高阶函数。
  • 如何使用 F# 尾递归匹配二叉树中的模式并检索合格的树节点?用递归方式编写似乎比使用高阶函数容易得多。
【解决方案2】:

按照偏好和一般编程风格,我将编写代码如下:

地图/折叠(如果可用)

let x = [1 .. 10] |> List.map ((*) 2)

它只是方便易用。

非尾递归函数

> let rec map f = function
    | x::xs -> f x::map f xs
    | [] -> [];;

val map : ('a -> 'b) -> 'a list -> 'b list

> [1 .. 10] |> map ((*) 2);;
val it : int list = [2; 4; 6; 8; 10; 12; 14; 16; 18; 20]

大多数算法在没有尾递归的情况下最容易阅读和表达。当您不需要太深地递归时,这尤其适用,使其适用于许多排序算法和平衡数据结构上的大多数操作。

记住,log2(1,000,000,000,000,000) ~= 50,所以没有尾递归的 log(n) 操作一点也不可怕。

带累加器的尾递归

> let rev l =
    let rec loop acc = function
        | [] -> acc
        | x::xs -> loop (x::acc) xs
    loop [] l

let map f l =
    let rec loop acc = function
        | [] -> rev acc
        | x::xs -> loop (f x::acc) xs
    loop [] l;;

val rev : 'a list -> 'a list
val map : ('a -> 'b) -> 'a list -> 'b list

> [1 .. 10] |> map ((*) 2);;
val it : int list = [2; 4; 6; 8; 10; 12; 14; 16; 18; 20]

它可以工作,但是代码很笨拙,算法的优雅有点模糊。上面的例子读起来还不错,但是一旦你进入树状数据结构,它就真的开始变成一场噩梦了。

尾递归与继续传递

> let rec map cont f = function
    | [] -> cont []
    | x::xs -> map (fun l -> cont <| f x::l) f xs;;

val map : ('a list -> 'b) -> ('c -> 'a) -> 'c list -> 'b

> [1 .. 10] |> map id ((*) 2);;
val it : int list = [2; 4; 6; 8; 10; 12; 14; 16; 18; 20]

每当我看到这样的代码时,我都会对自己说“现在这是一个巧妙的技巧!”。以可读性为代价,它保持了非递归函数的形状,并发现它对tail-recursive inserts into binary trees 非常有趣。

这可能是我对 monad 的恐惧症,也可能是我对 Lisp 的 call/cc 天生缺乏熟悉,但我认为 CSP 真正简化算法的那些场合很少见。 cmets欢迎反例。

While 循环/for 循环

我突然想到,除了序列推导之外,我从未在 F# 代码中使用过 while 或 for 循环。无论如何...

> let map f l =
    let l' = ref l
    let acc = ref []
    while not <| List.isEmpty !l' do
        acc := (!l' |> List.hd |> f)::!acc
        l' := !l' |> List.tl
    !acc |> List.rev;;

val map : ('a -> 'b) -> 'a list -> 'b list

> [1 .. 10] |> map ((*) 2);;
val it : int list = [2; 4; 6; 8; 10; 12; 14; 16; 18; 20]

它实际上是对命令式编程的模仿。您可以通过声明 let mutable l' = l 来保持一点理智,但任何重要的功能都需要使用 ref

【讨论】:

    【解决方案3】:

    老实说,任何可以用循环解决的问题都已经是自然递归的问题了,因为最后你可以将两者都转换为(通常是有条件的)跳转。

    我相信在几乎所有必须编写显式循环的情况下,您都应该坚持使用尾调用。它只是更通用:

    • while 循环将您限制为一个循环体,而尾调用可以让您在“循环”运行时在许多不同的状态之间切换。
    • while 循环将您限制为一个条件来检查终止,通过尾递归,您可以使用任意复杂的匹配表达式等待将控制流分流到其他地方。
    • 您的尾部调用都返回有用的值,并可能产生有用的类型错误。 while 循环不返回任何内容,而是依靠副作用来完成工作。
    • While 循环不是一流的,而带有尾调用(或其中的 while 循环)的函数是一流的。可以检查本地范围内的递归绑定的类型。
    • 尾递归函数可以很容易地分解为使用尾调用以所需顺序相互调用的部分。这可能会使事情更容易阅读,并且如果您发现要从循环的中间开始,这将有所帮助。这不适用于 while 循环。

    总而言之,F# 中的 while 循环只有在您真的要在函数体内使用可变状态时才值得,重复执行相同的操作直到满足特定条件。如果循环通常有用或非常复杂,您可能希望将其分解到其他一些顶级绑定中。如果数据类型本身是不可变的(很多 .NET 值类型是),那么无论如何使用对它们的可变引用可能收获甚微。

    我想说的是,您应该只在 while 循环非常适合这项工作并且相对较短的特殊情况下使用 while 循环。在许多命令式编程语言中,while 循环经常被扭曲成不自然的角色,比如在 case 语句上重复驱动东西。避免这类事情,看看是否可以使用尾调用,或者更好的是,使用更高阶的函数来达到相同的目的。

    【讨论】:

      【解决方案4】:

      许多问题都具有递归性质,但是长时间的强制思考常常使我们看不到这一点。

      一般来说,我会在函数式语言中尽可能使用函数式技术——循环永远不会是函数式的,因为它们完全依赖于副作用。因此,在处理命令式代码或算法时,使用循环就足够了,但在函数式上下文中,它们被认为不是很好。

      函数技术不仅意味着递归,还意味着使用适当的高阶函数。

      因此,在对列表求和时,既不是 for 循环也不是递归函数,而是 fold 是无需重新发明轮子就能获得可理解代码的解决方案。

      【讨论】:

        【解决方案5】:

        对于不是自然递归的问题 .. 反正只是在一个while循环中转换它

        您自己回答了这个问题。 对递归问题使用递归,对本质上不起作用的事物使用循环。 只是总是想:哪个感觉更自然,哪个更具可读性。

        【讨论】:

          猜你喜欢
          • 2011-08-20
          • 2020-04-03
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-09-14
          • 2018-10-10
          • 1970-01-01
          相关资源
          最近更新 更多