F#中的While或Tail递归,何时使用?

F#中的While或Tail递归,何时使用?,f#,functional-programming,while-loop,tail-recursion,F#,Functional Programming,While Loop,Tail Recursion,好的,只是在F#中,我现在是这样理解的: 有些问题本质上是递归的(构建或读取一个树状结构来命名),然后使用递归。在这些情况下,最好使用尾部递归来中断堆栈 有些语言是纯函数式的,因此必须使用递归而不是while循环,即使问题本质上不是递归的 所以我的问题是:既然F#也支持命令式范式,你会在F#中使用尾部递归来解决那些不是自然递归的问题吗?特别是因为我读过编译器识别尾部递归,并且只是在while循环中转换它 如果是:为什么 对于不是自然递归的问题 .. 只是在一个while循环中转换它 你自己回

好的,只是在F#中,我现在是这样理解的:

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

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

所以我的问题是:既然F#也支持命令式范式,你会在F#中使用尾部递归来解决那些不是自然递归的问题吗?特别是因为我读过编译器识别尾部递归,并且只是在while循环中转换它

如果是:为什么

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

你自己回答的。 对递归问题使用递归,对本质上不起作用的问题使用循环。
只要想一想:哪一种感觉更自然,哪一种更可读。

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

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

虽然循环需要易变性和效果,尽管我并不反对适度地使用它们,特别是当封装在本地函数的上下文中时,但当您开始仅为循环引入效果时,有时确实会感觉程序混乱/丑陋

尾部递归通常具有需要额外累加器参数或连续传递样式的缺点。这会给程序带来额外的样板文件,以处理函数的启动条件

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

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


对于那些有兴趣了解更多关于F#中折叠的人,为什么不看看我在这一主题系列文章中的帖子呢?

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

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

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


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

根据偏好顺序和一般编程风格,我将编写如下代码:

地图/折叠(如果可用)

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(10000000000000)~=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]
>让rec map cont f=函数
|[]->cont[]
|x::xs->map(乐趣l->cont('c->'a)->'c列表->'b
>[1..10]|>地图id(*)2);;
val-it:int-list=[2;4;6;8;10;12;14;16;18;20]
每当我看到这样的代码时,我都会对自己说“这是一个巧妙的技巧!”。以可读性为代价,它保持了非递归函数的形状,并且发现它对我们来说非常有趣

在这里可能是我的单子恐惧症,也可能是我天生不熟悉Lisp的call/cc,但我认为CSP实际简化算法的情况很少。评论中欢迎反例

While loops/for loops

我突然想到,除了序列理解,我从来没有在我的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]
>让我们映射f l=
设l'=ref l
设acc=ref[]
而不是List.hd |>f):!行政协调会
l':=!l'|>List.tl
!acc |>列表修订版;;
val映射:('a->'b)->'a列表->'b列表
>[1..10]|>地图(*)2);;
val-it:int-list=[2;4;6;8;10;12;14;16;18;20]

这实际上是对命令式编程的模仿。通过声明
let mutable l'=l
可以保持一点理智,但是任何非平凡的函数都需要使用
ref

老实说,使用循环可以解决的任何问题都已经是一个自然的递归问题,因为最终可以将这两个问题转换为(通常是有条件的)跳转

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

  • while循环将您限制为一个循环体,而tail调用可以允许您在“循环”运行时在许多不同的状态之间切换
  • while循环将您限制为一个条件来检查终止,即尾部