I’ve got a sequence of chars where I want to replace only the first char with the upper-case version of that first char.
The sequence may have zero, one or more elements. If there is more than one element then the first element should be converted to upper-case while the rest should remain as they are. If there is only one element then that single char should always be converted to upper-case and returned as a single-element sequence. If there are no elements then the function should just evaluate to either the original sequence or a new empty sequence.
I don’t want the ‘sequence-ness’ of the collection to be ‘broken’ by converting to something else and then back to a sequence.
I’m not hugely bothered about how quickly, or otherwise, it works at the moment.
I’m not concerned about how the case conversion is done, only the replacement of the first sequence element is important.
I came up with a few different versions but none of them seem particularly ‘nice’ to me, except maybe titleCase6, but even that might not be the best way:
open System
let titleCase1 (chars : char seq) =
if chars |> Seq.isEmpty then
chars
else
let head = chars |> Seq.head
let tail = chars |> Seq.tail
tail |> Seq.insertAt 0 (head |> Char.ToUpper)
let titleCase2 chars =
if chars |> Seq.isEmpty then
chars
else
let head = chars |> Seq.head
chars |> Seq.updateAt 0 (head |> Char.ToUpper)
let titleCase3 chars =
chars
|> Seq.tryHead
|> Option.map Char.ToUpper
|> Option.map (fun h -> chars |> Seq.tail |> Seq.insertAt 0 h)
|> Option.defaultValue chars
let titleCase4 chars =
chars
|> Seq.tryHead
|> Option.map Char.ToUpper
|> Option.map (fun h -> chars |> Seq.tail |> Seq.append (seq { h }))
|> Option.defaultValue chars
let titleCase5 chars =
chars
|> Seq.tryHead
|> Option.map Char.ToUpper
|> Option.map (fun h -> seq { h ; yield! (chars |> Seq.tail) })
|> Option.defaultValue chars
let titleCase6 chars =
chars
|> Seq.mapi (fun i c -> if i = 0 then (c |> Char.ToUpper) else c )
let functions = [ titleCase1 ; titleCase2 ; titleCase3 ;titleCase4 ; titleCase5 ; titleCase6 ]
let sequences = [ seq { 'a'..'e' } ; seq { 'a' ; 'B' ; 'c' ; 'D' ; 'e' }; seq { 'a' } ; seq { 'A' } ; Seq.empty ]
let ofCharArray (chars : char array) = System.String(chars)
let results =
(functions, sequences)
||> List.allPairs
|> List.map (fun (f, sequence) -> sequence |> f |> Seq.toArray |> ofCharArray)
How ‘nice’ a function looks is certainly in the eyes of the beholder, but to me, a nice function fulfills its purpose while being both clear and concise.
I would argue that the example below is both clear (easy to follow) and concise (body of only 3 rows).
let titleCase7 chars =
match Seq.tryHead chars with
| Some c -> Seq.updateAt 0 (Char.ToUpper c) chars
| None -> chars
Although I would not argue that it is necessarily “better” than any of the versions you have already provided! They do the same thing, after all.
let chars = [ 'a'..'e' ]
let titleCaseList = function
| [] -> []
| [single] -> [ single |> Char.ToUpper ]
| head::tail -> (head |> Char.ToUpper) :: tail
…but for a sequence rather than a list, which would have been ‘nicer’ (to me anyway), but apparently cons won’t work with sequences, which is a shame but there’s probably a very good technical reason for it that I probably won’t understand.
Totally agree that the list pattern matching alternative is nicest! Although the single element list match can be skipped, since in that case, tail will be the empty list :
let titleCaseList = function
| [] -> []
| head::tail -> (head |> Char.ToUpper) :: tail
The reason for cons to only be available for lists has to do with the underlying data structure (at least partly). Lists in F# are implemented as linked lists, which mean it makes sense to work with the head of the list, since the rest of the list will be left unaffected.
Arrays are continuous blocks of memory and sequences are lazily evaluated, potentially infinite collections. In these cases, the cons operator does not make as much sense.
It is more ‘point-free’ than the original titleCase6 but not completely
In a ‘point-free’ function, none of the parameters are explicitly written out. Instead, only function composition is used.
In the example
let UpperStartSeq = Seq.mapi (fun i c → if i = 0 then Char.ToUpper c else c)
The sequence parameter is indeed left out, but in the inner function, both the i and c parameter are written out explicitly. In a truly point free function, we would not create an anonymous function with explicitly named parameters. (I don’t know how one would compose such a ‘point-free’ function in this particular case )
Worth to mention is that functions written in a purely point-free manner are often harder to read and debug, see Microsofts coding convensions for F#