F# 在没有第一个或最后一个元素的情况下优雅地折叠?

F# folding gracefully without the first or last element?

假设您有一个字符串列表:[ "a"; "b"; "c" ] 并且您想将其转换为单个字符串,如下所示: "a,b,c" 注意最后一个逗号丢失了。 我发现这种情况对我来说一次又一次地出现,想想所有不允许尾随逗号的编程语言,你正在构建某种代码生成器。 我通常会得到这样的结果:

let listOfThings = ["a";"b";"c"]
let folded =
    listOfThings
    |> List.map (fun i -> i + ",")
    |> List.fold (+) ""
    |> (fun s -> s.Substring(0, s.Length - 1))

我觉得已经有一些类似 fold 的函数了,因为这似乎是一个基本的用例,我只是想不出它的名字是什么,或者用什么名字来搜索它。

A fold 将折叠函数递归地应用于列表的所有值,从初始状态开始,在这种情况下您并不是特别想要。

使用将列表的头部作为其起始状态的 reduce 更简单:

listOfThings |> List.reduce (fun sum cur -> sum + "," + cur) // "a,b,c"

一个小缺点是,由于它使用列表头,因此使用空列表调用 reduce 会失败。您可以通过检查空列表来缓解这种情况。

如您所述,在没有任何内置函数的情况下,我们跳过为最后一个元素添加尾随逗号:

let rec join = function
| []    -> ""
| [x]   -> x
| x::xs -> x + ","  + join xs

["a"; "b"; "c"] |> join // a,b,c

然而,最有效的方法是使用 String.Join,它在内部使用 StringBuilder,而 reduce 为每次调用分配一个新字符串:

String.Join(",", listOfThings) // "a,b,c"

应用于列表元素的缩减必须与这些元素属于同一类型。相比之下,a fold 的累加器(也称为状态)可以是不同类型的,这样更通用。签名使其显而易见:

val reduce: ('a -> 'a -> 'a) -> 'a list -> 'a
val fold: ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a

一种可能的方法可能包括为列表的第一个元素(或最后一个元素,在 foldBack 的情况下)提供不同的折叠功能。在这里检查空列表也是明智的,因为它是 reduce.

let fold1 folderN folder0 state = function
| [] -> state
| x::xs -> List.fold folderN (folder0 state x) xs
// val fold1 :
//   folderN:('a -> 'b -> 'a) ->
//     folder0:('a -> 'b -> 'a) -> state:'a -> _arg1:'b list -> 'a

现在我们可以折叠成一个列表,甚至可以使用 StringBuilder:

([], ["a";"b";"c"])
||> fold1 
    (fun s t -> t::", "::s)
    (fun s t -> t::s)
|> List.rev
// val it : string list = ["a"; ", "; "b"; ", "; "c"]

(System.Text.StringBuilder(), ["a";"b";"c"])
||> fold1 
    (fun s t -> s.Append(", ").Append t)
    (fun s t -> s.Append t) 
|> string
// val it : string = "a, b, c"

确实,有一个内置函数可以执行此操作:

let s = [ "a"; "b"; "c" ]
String.concat ", " s // "a, b, c"