按照偏好和一般编程风格,我将编写如下代码:
地图/折叠(如果可用)
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]
大多数算法在没有尾递归的情况下最容易阅读和表达。当您不需要太深地递归时,这特别有效,使其适用于许多排序算法和平衡数据结构上的大多数操作。
请记住,log 2 (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 或 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
.