虽然在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
都是在不重新发明轮子的情况下编写可理解代码的解决方案。