Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is using a StringBuilder a right thing to do in F#?

StringBuiler is a mutable object, F# encourages employing immutability as much as possible. So one should use transformation rather than mutation. Does this apply to StringBuilder when it comes to building a string in F#? Is there an F# immutable alternative to it? If so, is this alternative as efficient?

A snippet

like image 895
Trident D'Gao Avatar asked Sep 03 '13 15:09

Trident D'Gao


2 Answers

I think that using StringBuilder in F# is perfectly fine - the fact that sb.Append returns the current instance of StringBuilder means that it can be easily used with the fold function. Even though this is still imperative (the object is mutated), it fits reasonably well with the functional style when you do not expose references to StringBuilder.

But equally, you can just construct a list of strings and concatenate them using String.concat - this is almost as efficient as using StringBuilder (it is slower, but not much - and it is significantly faster than concatenating strings using +)

So, lists give you similar performance, but they are immutable (and work well with concurrency etc.) - they would be a good fit if you were building string algorithmically, because you can just append strings to the front of the list - this is very efficient operation on lists (and then reverse the string). Also, using list expressions gives you a very convenient syntax:

// Concatenating strings using + (2.3 seconds)
let s1 = [ for i in 0 .. 25000 -> "Hello " ] |> Seq.reduce (+)
s1.Length

// Creating immutable list and using String.concat (5 ms)
let s2 = [ for i in 0 .. 25000 -> "Hello " ] |> String.concat ""
s2.Length

// Creating a lazy sequence and concatenating using StringBuilder & fold (5 ms)
let s3 = 
  seq { for i in 0 .. 25000 -> "Hello " }
  |> Seq.fold(fun (sb:System.Text.StringBuilder) s -> 
      sb.Append(s)) (new System.Text.StringBuilder())
  |> fun x -> x.ToString()
s3.Length

// Imperative solution using StringBuilder and for loop (1 ms)
let s4 = 
  ( let sb = new System.Text.StringBuilder()
    for i in 0 .. 25000 do sb.Append("Hello ") |> ignore
    sb.ToString() )
s4.Length

The times were measured on my, fairly fast, work machine using #time in F# Interactive - it is quite likely that it would be faster in release build, but I think they are fairly representative.

like image 98
Tomas Petricek Avatar answered Nov 17 '22 21:11

Tomas Petricek


If you have need of high performance sting concatenation, then the string builder is probably the right way to go, however, there are ways to make the string builder more functional. Generally speaking, if you need mutability in a functional program, the appropriate way to do this is to create a functional wrapper for it. In F# this is typically expressed as a computation expression. There is an example of a string builder computation expression here.

Example Usage:

//Create a function which builds a string from an list of bytes
let bytes2hex (bytes : byte []) =
    string {
        for byte in bytes -> sprintf "%02x" byte
    } |> build

//builds a string from four strings
string {
        yield "one"
        yield "two"
        yield "three"
        yield "four"
    } |> build

Edit: I made a new implementation of the above computation expression and then ran a release version of Tomas' four solutions plus my computation expression and the computation expression I previously linked.

s1 elapsed Time: 128150 ms  //concatenation
s2 elapsed Time: 459 ms     //immutable list + String.concat
s3 elapsed Time: 354 ms     //lazy sequence and concatenating using StringBuilder & fold 
s4 elapsed Time: 39 ms      //imperative
s5 elapsed Time: 235 ms     //my computation expression
s6 elapsed Time: 334 ms     //the linked computation expression

Notice that s3 takes 9 times as long as the imperative while s5 only takes 6 times as long.

Here is my implementation of the string builder computation expression:

open System.Text

type StringBuilderUnion =
| Builder of StringBuilder
| StringItem of string

let build = function | Builder(x) -> string x | StringItem(x) -> string x

type StringBuilderCE () =
    member __.Yield (txt : string) = StringItem(txt)
    member __.Yield (c : char) = StringItem(c.ToString())
    member __.Combine(f,g) = Builder(match f,g with
                                     | Builder(F),   Builder(G)   ->F.Append(G.ToString())
                                     | Builder(F),   StringItem(G)->F.Append(G)
                                     | StringItem(F),Builder(G)   ->G.Insert(0, F)
                                     | StringItem(F),StringItem(G)->StringBuilder(F).Append(G))
    member __.Delay f = f()
    member __.Zero () = StringItem("")
    member __.For (xs : 'a seq, f : 'a -> StringBuilderUnion) =
                    let sb = StringBuilder()
                    for item in xs do
                        match f item with
                        | StringItem(s)-> sb.Append(s)|>ignore
                        | Builder(b)-> sb.Append(b.ToString())|>ignore
                    Builder(sb)

let builder1 = new StringBuilderCE ()

Timer function (note that each test is run 100 times):

let duration f = 
    System.GC.Collect()
    let timer = new System.Diagnostics.Stopwatch()
    timer.Start()
    for _ in 1..100 do
        f() |> ignore
    printfn "elapsed Time: %i ms" timer.ElapsedMilliseconds
like image 29
N_A Avatar answered Nov 17 '22 20:11

N_A