From e8a7a4dc62ce70df17c00262b36a7b4a3dcfc175 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Thu, 18 Jan 2024 15:03:25 +0100 Subject: [PATCH] reverse-string: add approaches --- .../reverse-string/.approaches/config.json | 15 ++------ .../.approaches/introduction.md | 23 +++++++---- .../.approaches/seq-module/content.md | 32 ++++------------ .../.approaches/span/content.md | 38 +++++++------------ .../.approaches/string-builder/content.md | 8 +--- .../.approaches/string-builder/snippet.txt | 11 +++--- 6 files changed, 46 insertions(+), 81 deletions(-) diff --git a/exercises/practice/reverse-string/.approaches/config.json b/exercises/practice/reverse-string/.approaches/config.json index 09823b7ae..4af3c91cc 100644 --- a/exercises/practice/reverse-string/.approaches/config.json +++ b/exercises/practice/reverse-string/.approaches/config.json @@ -6,7 +6,7 @@ }, "approaches": [ { - "uuid": "44a0fe16-9c7d-4481-9eae-b223ecd3f88e", + "uuid": "85f2dd83-247d-4549-a6b3-ff6c4356288d", "slug": "seq-module", "title": "Seq module", "blurb": "Use functions from the Seq module to concisely reverse a string.", @@ -15,16 +15,7 @@ ] }, { - "uuid": "8a2ddcd5-ffe7-4448-842e-25b302708b8c", - "slug": "array-reverse", - "title": "Array.Reverse", - "blurb": "Use Array.Reverse to reverse a string.", - "authors": [ - "erikschierboom" - ] - }, - { - "uuid": "f42fa12e-e9b8-4aaa-9fc2-188c67d05809", + "uuid": "c4c26760-3398-46ba-8d9c-445c7b17860a", "slug": "span", "title": "Span", "blurb": "Use Span and stack allocation for hyper-optimized string reversal.", @@ -33,7 +24,7 @@ ] }, { - "uuid": "f4f662d0-4917-4a89-a62a-12809a8c2b11", + "uuid": "fd88f91d-57e4-4e16-95b3-e8cf5046a82c", "slug": "string-builder", "title": "StringBuilder", "blurb": "Reverse a string using the StringBuilder class.", diff --git a/exercises/practice/reverse-string/.approaches/introduction.md b/exercises/practice/reverse-string/.approaches/introduction.md index c36a03a2f..521eb8647 100644 --- a/exercises/practice/reverse-string/.approaches/introduction.md +++ b/exercises/practice/reverse-string/.approaches/introduction.md @@ -40,19 +40,28 @@ let reverse (input: string) = This approach iterates over the string's characters backwards, building up the reverse string using a `StringBuilder`. For more information, check the [`StringBuilder` approach][approach-string-builder]. -## Which approach to use? +## Alternative approach: `Span` -If readability is your primary concern (and it usually should be), the `Seq` module approach is hard to beat. +```fsharp +let reverse (input: string) = + let memory = NativePtr.stackalloc(input.Length) |> NativePtr.toVoidPtr + let span = Span(memory, input.Length) + + for i in 0..input.Length - 1 do + span[input.Length - 1 - i] <- input[i] -The `Array.Reverse()` approach is the best performing apporach. -For a more detailed breakdown, check the [performance article][article-performance]. + span.ToString() +``` + +This approach uses the `Span` type, which is a highly optimized type designed to have great performance. +For more information, check the [`Span` approach][approach-span]. -The `StringBuilder` approach has the worst performance of the listed approach, and is more error-prone to write as it has to deal with lower and upper bounds checking. +## Which approach to use? + +If readability is your primary concern (and it usually should be), the `Seq` module approach is hard to beat. [constructor-array-chars]: https://learn.microsoft.com/en-us/dotnet/api/system.string.-ctor -[article-performance]: https://exercism.org/tracks/fsharp/exercises/reverse-string/articles/performance [approach-seq-module]: https://exercism.org/tracks/fsharp/exercises/reverse-string/approaches/seq-module -[approach-array-reverse]: https://exercism.org/tracks/fsharp/exercises/reverse-string/approaches/array-reverse [approach-span]: https://exercism.org/tracks/fsharp/exercises/reverse-string/approaches/span [approach-string-builder]: https://exercism.org/tracks/fsharp/exercises/reverse-string/approaches/string-builder [seq-module]: https://fsharp.github.io/fsharp-core-docs/reference/fsharp-collections-seqmodule.html diff --git a/exercises/practice/reverse-string/.approaches/seq-module/content.md b/exercises/practice/reverse-string/.approaches/seq-module/content.md index aa6354de5..0eec238da 100644 --- a/exercises/practice/reverse-string/.approaches/seq-module/content.md +++ b/exercises/practice/reverse-string/.approaches/seq-module/content.md @@ -10,32 +10,14 @@ let reverse input = |> System.String ``` -The `string` class implements the `IEnumerable` interface, which allows us to call [LINQ][linq]'s [`Reverse()`][linq-reverse] extension method on it. +The `string` class implements the `seq` interface (which is an abbreviation of the CLI `IEnumerable` interface), which means we can use functions from the [`Seq` module][seq-module] on it. -To convert the `IEnumerable` returned by `Reverse()` back to a `string`, we first use [`ToArray()`][linq-to-array] to convert it to a `char[]`. +First, we pipe the input `string` into [`Seq.reverse`][seq.rev], which returns an enumerable with the input in reverse order. -Finally, we return the reversed `string` by calling its constructor with the (reversed) `char[]`. +To convert the `seq` returned by `Seq.reverse` back to a `string`, we first use [`Seq.toArray`][seq.toArray] to convert it to a `char[]`. -## Shortening +Finally, we convert the `char` array back to a `string` by piping it into the `System.String` constructor. -There are two things we can do to further shorten this method: - -1. Remove the curly braces by converting to an [expression-bodied method][expression-bodied-method] -1. Use a [target-typed new][target-typed-new] expression to replace `new string` with just `new` (the compiler can figure out the type from the method's return type) - -Using this, we end up with: - -```fsharp -public static string Reverse(string input) => new(input.Reverse().ToArray()); -``` - -## Performance - -If you're interested in how this approach's performance compares to other approaches, check the [performance approach][approach-performance]. - -[linq-reverse]: https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.reverse -[linq-to-array]: https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.toarray -[expression-bodied-method]: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/expression-bodied-members#methods -[linq]: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/linq/ -[target-typed-new]: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/target-typed-new -[approach-performance]: https://exercism.org/tracks/csharp/exercises/reverse-string/articles/performance +[seq-module]: https://fsharp.github.io/fsharp-core-docs/reference/fsharp-collections-seqmodule.html +[seq.rev]: https://fsharp.github.io/fsharp-core-docs/reference/fsharp-collections-seqmodule.html#rev +[seq.toArray]: https://fsharp.github.io/fsharp-core-docs/reference/fsharp-collections-seqmodule.html#toArray diff --git a/exercises/practice/reverse-string/.approaches/span/content.md b/exercises/practice/reverse-string/.approaches/span/content.md index 858c4a5af..24e601efa 100644 --- a/exercises/practice/reverse-string/.approaches/span/content.md +++ b/exercises/practice/reverse-string/.approaches/span/content.md @@ -16,32 +16,35 @@ let reverse (input: string) = span.ToString() ``` -C# 7.2. introduced the [`Span`][span-t] class, which was specifically designed to allow performant iteration/mutation of _array-like_ objects. +F# 4.5 introduced support for the [`Span`][span-t] class, which was specifically designed to allow performant iteration/mutation of _array-like_ objects. The `Span` class helps improve performance by always being allocated on the _stack_, and not the _heap_. As objects on the stack don't need to be garbage collected, this can help improve performance (check [this blog post][using-span-t] for more information). How can we leverage `Span` to reverse our `string`? The `string` class has an [`AsSpan()`][string-as-span] method, but that returns a `ReadOnlySpan`, which doesn't allow mutation (otherwise we'd be able to indirectly modify the `string`). + We can work around this by manually allocating a `char[]` and assigning to a `Span`: ```fsharp -Span chars = new char[input.Length]; -for (var i = 0; i < input.Length; i++) -{ - chars[input.Length - 1 - i] = input[i]; -} -return new string(chars); +let array = Array.zeroCreate(input.Length) +let span = Span(array) + +for i in 0..input.Length - 1 do + span[input.Length - 1 - i] <- input[i] + +span.ToString() ``` After creating `Span`, we use a regular `for`-loop to iterate over the string's characters and assign them to the right position in the span. Finally, we can use the `string` constructor overload that takes a `Span` to create the `string`. However, this is basically the same approach as the `Array.Reverse()` approach, but with us also having to manually write a `for`-loop. -We _can_ do one better though, and that is to use [`stackalloc`][stackalloc]. +We _can_ do one better though, and that is to use [`NativePtr.stackalloc`]nativeptr.stackalloc. With `stackalloc`, we can assign a block of memory _on the stack_ (whereas the array would be stored on the heap). ```fsharp -Span chars = stackalloc char[input.Length]; +let memory = NativePtr.stackalloc(input.Length) |> NativePtr.toVoidPtr +let span = Span(memory, input.Length) ``` With this version, the memory allocated for the `Span` is all on the stack and no garbage collection is needed for that data. @@ -55,22 +58,7 @@ So what is the limit for the amount of memory we can allocate? Well, this depends on how memory has already been allocated on the stack. That said, a small test program successfully stack-allocated memory for `750_000` characters, so you might be fine. -## Alternative - -It is possible to use an alternative span-based implementation that is more readable, but has the downside of being about twice as slow: - -```fsharp -Span chars = stackalloc char[input.Length]; -input.AsSpan().CopyTo(chars); -chars.Reverse(); -return new string(chars); -``` - -## Performance - -If you're interested in how this approach's performance compares to other approaches, check the [performance approach][approach-performance]. - -[stackalloc]: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/stackalloc +[nativeptr.stackalloc]: https://fsharp.github.io/fsharp-core-docs/reference/fsharp-nativeinterop-nativeptrmodule.html#stackalloc [using-span-t]: https://learn.microsoft.com/en-us/archive/msdn-magazine/2018/january/csharp-all-about-span-exploring-a-new-net-mainstay [span-t]: https://learn.microsoft.com/en-us/dotnet/api/system.span-1 [string-as-span]: https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.asspan diff --git a/exercises/practice/reverse-string/.approaches/string-builder/content.md b/exercises/practice/reverse-string/.approaches/string-builder/content.md index dca3b5ebe..01d1f44ef 100644 --- a/exercises/practice/reverse-string/.approaches/string-builder/content.md +++ b/exercises/practice/reverse-string/.approaches/string-builder/content.md @@ -20,14 +20,10 @@ A `StringBuilder` is often overkill when used to create short strings, but can b ``` The first step is to create a `StringBuilder`. -We then use a `for`-loop to walk through the string's characters in reverse order, appending them to the `StringBuilder` via its [`Append()`][string-builder-append] method. +We then use a `for`-loop to walk through the string's characters in reverse order via the [`Seq.rev` function][seq.rev], appending them to the `StringBuilder` via its [`Append()`][string-builder-append] method. Finally, we return the reversed `string` by calling the `ToString()` method on the `StringBuilder` instance. -## Performance - -If you're interested in how this approach's performance compares to other approaches, check the [performance approach][approach-performance]. - [string-builder]: https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder [string-builder-append]: https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.append -[approach-performance]: https://exercism.org/tracks/csharp/exercises/reverse-string/articles/performance +[seq.rev]: https://fsharp.github.io/fsharp-core-docs/reference/fsharp-collections-seqmodule.html#rev diff --git a/exercises/practice/reverse-string/.approaches/string-builder/snippet.txt b/exercises/practice/reverse-string/.approaches/string-builder/snippet.txt index 20cfee9d2..5c385145d 100644 --- a/exercises/practice/reverse-string/.approaches/string-builder/snippet.txt +++ b/exercises/practice/reverse-string/.approaches/string-builder/snippet.txt @@ -1,6 +1,5 @@ -var chars = new StringBuilder(); -for (var i = input.Length - 1; i >= 0; i--) -{ - chars.Append(input[i]); -} -return chars.ToString(); +let reverse (input: string) = + let chars = StringBuilder() + for char in Seq.rev input do + chars.Append(char) |> ignore + chars.ToString() \ No newline at end of file