虽然在F#或尾递归,什么时候使用?

好吧,只有在F#中,这就是我现在的理解:

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

  • 有些语言是纯功能的,所以你必须用递归来代替while循环,即使这个问题在本质上不是递归的

  • 所以我的问题是:既然F#也支持命令范式,你会在F#中使用尾递归来处理那些不是自然递归的问题吗? 特别是因为我已经阅读了编译器recongnizes尾递归,只是转换它在一个while循环吗?

    如果是这样:为什么?


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

    在循环和尾递归方面都有一些丑陋。

    while循环需要可变性和效果,尽管我没有什么反对在适度使用它们,特别是当封装在本地函数的上下文中时,当你开始仅将效果引入循环时,有时候感觉就像你在混乱/丑化程序。

    尾递归通常具有需要额外累加器参数或延续传递样式的缺点。 这会使程序变得杂乱无章,以便按照功能的启动条件。

    最好的答案是既不使用while循环也不使用递归。 高阶函数和lambda是你的救星,特别是地图和褶皱。 为什么在混乱的控制结构中进行循环时,你可以将它们封装在可重用的库中,然后简单地声明你的计算的本质?

    如果您习惯于经常调用map / fold而不是使用循环/递归,并且提供折叠函数以及您介绍的任何新的树形结构数据类型,那么您将走得更远。 :)

    对于那些有兴趣了解更多关于F#中折叠的人,为什么不查看关于该主题的系列文章中的前三篇博文?


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

    如果可用,则映射/折叠

    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]
    

    每当我看到这样的代码时,我都会对自己说:“现在这是一个巧妙的技巧!”。 以可读性为代价,它保持了非递归函数的形状,并且发现它对于二进制树中的尾递归插入非常有趣。

    它可能是我在这里讲的monad-phobia,或者可能是我对Lisp的call / cc缺乏熟悉程度,但我认为这些时候CSP实际上简化了算法的情况并不多见。 在评论中欢迎反例。

    while循环/ for循环

    我发现,除了序列理解之外,我从来没有在F#代码中使用while循环。 任何状况之下...

    > 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


    许多问题具有递归性质,但经过长时间的思考往往阻止我们看到这一点。

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

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

    因此,在总结一个列表时,无论是for循环还是递归函数,而是一个fold都是在不重新发明轮子的情况下编写可理解代码的解决方案。

    链接地址: http://www.djcxy.com/p/14129.html

    上一篇: While or Tail Recursion in F#, what to use when?

    下一篇: Allocating an 2D array in C in heap instead of stack