Reputation: 568
Say you have a list of strings: [ "a"; "b"; "c" ] and you want to transform it to a single string like so: "a,b,c" notice that the last comma is missing. I find this case comes up again and again for me, think of all the programming languages that do not allow the trailing comma to be there, and you are building some kind of code generator. I usually end up with something like:
let listOfThings = ["a";"b";"c"]
let folded =
listOfThings
|> List.map (fun i -> i + ",")
|> List.fold (+) ""
|> (fun s -> s.Substring(0, s.Length - 1))
I feel like there is some fold like function already because this seems such a basic use case, and I just can't figure out what would be it's name, or by what name to search for it.
Upvotes: 2
Views: 543
Reputation: 8551
Indeed, there is a built in function that does this:
let s = [ "a"; "b"; "c" ]
String.concat ", " s // "a, b, c"
Upvotes: 0
Reputation: 12667
A fold
applies your folding function recursively over all values of the list, starting with an initial state, which you don't particularly want in this case.
It's simpler to use a reduce which uses the list's head as its starting state:
listOfThings |> List.reduce (fun sum cur -> sum + "," + cur) // "a,b,c"
A minor drawback is that since it uses the list head, calling reduce with an empty list would fail. You can mitigate that with a check for an empty list.
Without any built-ins, as you have described, we skip the addition of the trailing comma for the last element:
let rec join = function
| [] -> ""
| [x] -> x
| x::xs -> x + "," + join xs
["a"; "b"; "c"] |> join // a,b,c
However, the most efficient method would be to use String.Join
which internally uses a StringBuilder
, while reduce
allocates a new string for every call:
String.Join(",", listOfThings) // "a,b,c"
Upvotes: 5
Reputation: 5741
A reduction applied to the elements of a list is necessarily of the same type as these are. In contrast, the accumulator (also called state) of a fold
can be of a different type, which is more versatile. The signatures make it apparent:
val reduce: ('a -> 'a -> 'a) -> 'a list -> 'a val fold: ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a
A possible approach might consist in provision of a different folding function for the first element of the list (or for the last, in the case of foldBack
). Here it is also prudent to check for an empty list, as it is with 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
Now we can fold into a list, or even use 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"
Upvotes: 3