F#: Convert sequence to comma separated string
I've been continuing playing around with parsing Cruise data as I mentioned yesterday with the goal today being to create a graph from the build data.
After recommendations from Dean Cornish and Sam Newman on Twitter I decided to give the Google Graph API a try to do this and realised that I would need to create a comma separated string listing all the build times to pass to the Google API.
My initial thinking was that I could just pipe the sequence of values through 'Seq.fold' and add a comma after each value:
let ConvertToCommaSeparatedString (value:seq<string>) = let initialAttempt = value |> Seq.fold (fun acc x -> acc + x + ",") "" initialAttempt.Remove(initialAttempt.Length-1)
It works but you end up with a comma after the last value as well and then need to remove that on the next line which feels very imperative to me.
My next thought was that maybe I would be able to do this by making use of a recursive function which matched the sequence on each iteration and then when it was on the last value in the list to not add the comma.
I know how to do this for a list so I decided to go with that first:
let ConvertToCommaSeparatedString (value:seq<string>) = let rec convert (innerVal:List<string>) acc = match innerVal with | [] -> acc | hd::[] -> convert [] (acc + hd) | hd::tl -> convert tl (acc + hd + ",") convert (Seq.to_list value) ""
That works as well but it seems a bit weird that we need to convert everything in a list to do it.
A bit of googling revealed an interesting post by Brian McNamara where he suggests creating an active pattern which would cast the 'seq' to a 'LazyList' (which is deprecated but won't be removed apparently) and then do some pattern matching against that instead.
The active pattern which Brian describes is like this:
let rec (|SeqCons|SeqNil|) (s:seq<'a>) = match s with | :? LazyList<'a> as l -> match l with | LazyList.Cons(a,b) -> SeqCons(a,(b :> seq<_>)) | LazyList.Nil -> SeqNil | _ -> (|SeqCons|SeqNil|) (LazyList.of_seq s :> seq<_>)
This doesn't cover the three states of the sequence which I want to match so I adjusted it slightly to do what I want:
let rec (|SeqCons|SeqNil|SeqConsLastElement|) (s:seq<'a>) = match s with | :? LazyList<'a> as l -> match l with | LazyList.Cons(a,b) -> match b with | LazyList.Nil -> SeqConsLastElement(a) | LazyList.Cons(_,_) -> SeqCons(a,(b :> seq<_>)) | LazyList.Nil -> SeqNil | _ -> (|SeqCons|SeqNil|SeqConsLastElement|) (LazyList.of_seq s :> seq<_>)
Our function to convert sequences to a comma separated string would now look like this:
let ConvertToCommaSeparatedString (value:seq<string>) = let rec convert (innerVal:seq<string>) acc = match innerVal with | SeqNil -> acc | SeqConsLastElement(hd) -> convert [] (acc + hd) | SeqCons(hd,tl) -> convert tl (acc + hd + ",") convert (value) ""
An example of this in action would be like this:
ConvertToCommaSeparatedString (seq { yield "mark"; yield "needham" });;
val it : string = "mark,needham"
I like your last solution, but you could also check if the current state of acc and conditionally add the comma 'in front' with the 2nd entry. fun acc x -> (if acc "" then "," else "") + x…if's are expressions.
roger
9 Jul 09 at 11:52 pm
Dolphin Smalltalk does it like this which I thought was quite clever. I know this is not a functional style.
do: operation separatedBy: separator
"Evaluate the argument, operation, for each of the
receiver's elements, interspersed with evaluations of the
argument, separator. The separator is first evaluated after the first
element, and is not evaluated after the last element (i.e. it is not evaluated
at all if there are less than two elements)."
| sep |
sep := [sep := separator]. "Switch to the real separator after first eval."
self do:
[:each |
sep value.
operation value: each]
Squeak and most other Smalltalks do something like the following:
do: elementBlock separatedBy: separatorBlock
"Evaluate the elementBlock for all elements in the receiver,
and evaluate the separatorBlock between."
| beforeFirst |
beforeFirst := true.
self do:
[:each |
beforeFirst
ifTrue: [beforeFirst := false]
ifFalse: [separatorBlock value].
elementBlock value: each]
Carlo
10 Jul 09 at 6:47 pm
Hello,
I believe that String.Join Method could the job, too.
_St
31 Jul 09 at 6:42 pm
let reduceDefault f z s = if Seq.isEmpty s then z else Seq.reduce f s let ConvertToCommaSeparatedString s = reduceDefault (fun x y -> x+","+y) "" sroger, nice trick – but it's wrong if your sequence starts with at least one empty string.
Jonathan Graehl
1 Aug 09 at 3:22 pm