2013-09-03 23 views
23

StringBuiler jest obiektem zmiennym, F # zachęca do stosowania niezmienności tak bardzo, jak to możliwe. Tak więc należy używać transformacji zamiast mutacji. Czy to dotyczy StringBuilder, jeśli chodzi o budowanie ciąg w F #? Czy istnieje niezmienna alternatywa F #? Jeśli tak, to czy ta alternatywa jest równie skuteczna?Czy korzystanie z StringBuilder jest słuszne w F #?

A snippet

+1

Można użyć funkcji DList http://book.realworldhaskell.org/read/data-structures.html#data.dlist http://jackfoxy.com/f-data-structures/fsharpx-datastructures/#id35 –

+0

Napisałem [niezmienny konstruktor napisów] (http: // stackoverflow.com/a/8346765/162396) w odpowiedzi na wcześniejsze pytanie. Test Tomasa przebiega w ciągu 18 ms (nasze maszyny muszą być podobne, ponieważ otrzymuję te same timingi dla innych wersji). – Daniel

+0

@MauricioScheffer Byłbym dość zainteresowany, aby wiedzieć, co byłoby porównanie DList i prostej listy z cofaniem. Podejrzewam, że wywoływanie funkcji w DList może również kosztować ... –

Odpowiedz

37

myślę, że za pomocą StringBuilder w F # jest idealnie w porządku - fakt, że sb.Append zwraca bieżące wystąpienie StringBuilder oznacza, że ​​można go łatwo użyć z funkcją fold. Mimo że jest to wciąż konieczne (obiekt jest zmutowany), pasuje on do rozsądnie w stylu funkcjonalnym, gdy nie ujawnia się odniesień do StringBuilder.

Ale równie, można po prostu skonstruować listę ciągów i złączyć je za pomocą String.concat - to prawie tak skuteczne jak przy użyciu StringBuilder (jest wolniejszy, ale nie za dużo - i to znacznie szybciej niż łączenie ciągów za pomocą +)

Listy dają podobną wydajność, ale są niezmienne (i działają dobrze przy współbieżności itd.) - byłyby dobre, gdybyś budował algorytmicznie łańcuch, ponieważ możesz po prostu dodać ciągi do początku listy - jest to bardzo wydajna operacja na listach (a następnie odwrócenie ciągu znaków). Ponadto, za pomocą listy wyrażeń daje bardzo wygodny składnię:

// 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 

Czasy mierzono w moim, dość szybki, maszyny roboczej z wykorzystaniem #time w F # Interactive - jest całkiem prawdopodobne, że będzie to szybciej budować uwalnianiu ale myślę, że są dość reprezentatywne.

+0

Czy s2 ma w sobie 'List.rev' przed' String.concat'? Jak zauważyłeś powyżej, lista będzie prawdopodobnie skonstruowana w taki sposób, że elementy są w odwrotnej kolejności, w jakiej chcesz je połączyć. – mydogisbox

7

Jeśli potrzebują wysokiej wydajności żądło konkatenacji, a następnie budowniczym ciąg jest chyba właściwa droga, jednak istnieją sposoby, aby budowniczy ciąg bardziej funkcjonalne. Mówiąc ogólnie, jeśli potrzebujesz zmiany w funkcjonalnym programie, właściwym sposobem na to jest stworzenie funkcjonalnego opakowania dla niego. W języku F # jest to zwykle wyrażane jako wyrażenie obliczeniowe. Istnieje przykład wyrażenia obliczającego konstruktora ciągów znaków here.

Przykład użycia:

//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: Zrobiłem nową realizację powyższej wypowiedzi obliczeń, a następnie prowadził wersji systemu czterech rozwiązań Tomasa plus moje wypowiedzi obliczeń i ekspresję obliczeń ja wcześniej powiązany.

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 

Zauważ, że s3 zajmuje 9 razy więcej czasu niż imperatyw, podczas gdy s5 zajmuje tylko 6 razy więcej.

Oto moja realizacja wyrażeniu ciąg obliczeń konstruktora:

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() 

funkcja timera (należy pamiętać, że każde badanie przeprowadzane jest 100 razy):

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 
+0

Tylko specjalny przypadek Czytnika https://github.com/fsharp/fsharpx/issues/201 –

+1

@MauricioScheffer Po przejrzeniu rzeczywistej realizacji, myślę, że to jest kiepskie sposób implementacji konstruktora ciągów, ponieważ traci on wszystkie zalety wydajności klasy 'StringBuilder'. Patrzę na tworzenie wersji, która ma właściwości wydajności blisko korzystania z surowego 'StringBuilder'. – mydogisbox

+0

Jedynym osiągnięciem wydajności jest użycie 'sprintf', które jest całkowicie opcjonalne. Nie widzę żadnego innego problemu z wydajnością (mówienie o czytniku) –