diff --git a/exercises/practice/reverse-string/.approaches/config.json b/exercises/practice/reverse-string/.approaches/config.json new file mode 100644 index 000000000..6316f3d16 --- /dev/null +++ b/exercises/practice/reverse-string/.approaches/config.json @@ -0,0 +1,36 @@ +{ + "introduction": { + "authors": [ + "michalporeba" + ] + }, + "approaches": [ + { + "uuid": "0ec50f64-a8f7-49b5-b0d0-3977149c23ba", + "slug": "string-builder", + "title": "StringBuilder", + "blurb": "Use the StringBuilder class.", + "authors": [ + "michalporeba" + ] + }, + { + "uuid": "9cc22152-82c3-49b1-95a7-8ee62cdeac21", + "slug": "sequence", + "title": "Use the fact that a string is also a sequence", + "blurb": "Reverse the string as a sequence.", + "authors": [ + "michalporeba" + ] + }, + { + "uuid": "533b4795-7f1f-4048-b022-9cc500eebe62", + "slug": "recursion", + "title": "Recursion", + "blurb": "Reverse the string recursively.", + "authors": [ + "michalporeba" + ] + } + ] +} diff --git a/exercises/practice/reverse-string/.approaches/introduction.md b/exercises/practice/reverse-string/.approaches/introduction.md new file mode 100644 index 000000000..0b160f62d --- /dev/null +++ b/exercises/practice/reverse-string/.approaches/introduction.md @@ -0,0 +1,53 @@ +# Introduction + +[Strings][string] in Clojure are immutable, which means we cannot reverse them in place. +Instead, typically, we will create a new string while reversing the original one. + +## String builder + +One way to work around it is to use a string builder from the underlying Java Virtual Machine. + +```clojure +(defn reverse-string [s] + (.toString + (.reverse + (StringBuilder. s)))) +``` + +Let's look at the [string builder approach][string-builder-approach] and a shortcut to it. + +## It's a sequence + +Beyond the above, there are a great many different solutions, but in general, they depend on two facts. +[Strings][string] in Clojure are Java [string][java-string]s. +Many core Clojure functions call `seq` on their arguments automatically converting a string into a sequence of characters. +And there are many ways to reverse a sequence. + +```clojure +(defn reverse-string [s] + (apply str (reverse s))) +``` + +We discuss some variations in [It's a sequence approach][sequence-approach]. + +## Recursion + +A distinct variation of the above is to process a sequence in a recursive function. + +```clojure +(defn reverse-string [s] + (loop [s s acc ""] + (if (empty? s) + acc + (recur + (rest s) + (str (first s) acc))))) +``` + +Let's explore the [recursive approach][recursive-approach]. + +[string]: https://clojure-doc.org/articles/cookbooks/strings +[java-string]: https://docs.oracle.com/javase/8/docs/api/java/lang/String.html +[string-builder-approach]: https://exercism.org/tracks/clojure/exercises/reverse-string/approaches/string-builder +[sequence-approach]: https://exercism.org/tracks/clojure/exercises/reverse-string/approaches/its-a-sequence +[recursive-approach]: https://exercism.org/tracks/clojure/exercises/reverse-string/approaches/recursion \ No newline at end of file diff --git a/exercises/practice/reverse-string/.approaches/recursion/content.md b/exercises/practice/reverse-string/.approaches/recursion/content.md new file mode 100644 index 000000000..94691ec53 --- /dev/null +++ b/exercises/practice/reverse-string/.approaches/recursion/content.md @@ -0,0 +1,28 @@ +# Recursion + +```clojure +(defn reverse-string [s] + (loop [s s acc ""] + (if (empty? s) + acc + (recur + (rest s) + (str (first s) acc))))) +``` + +## Performance considerations + +It is not necessarily bad to use recursion as shown in the example above. +In fact, many other approaches use recursion behind the scenes as, for instance, `clojure.string/join` is implemented using recursion. +However, we should remember that strings are immutable, so code like the above will be inefficient. +Instead, we should consider using the `StringBuilder` in the recursive function. For example, like so: + +```clojure +(defn reverse-string [s] + (loop [s (into () s) sb (StringBuilder. "")] + (if (empty? s) + (.toString sb) + (recur + (rest s) + (-> sb (.append (first s))))))) +``` \ No newline at end of file diff --git a/exercises/practice/reverse-string/.approaches/recursion/snippet.txt b/exercises/practice/reverse-string/.approaches/recursion/snippet.txt new file mode 100644 index 000000000..4e1759653 --- /dev/null +++ b/exercises/practice/reverse-string/.approaches/recursion/snippet.txt @@ -0,0 +1,7 @@ +(defn reverse-string [s] + (loop [s s acc ""] + (if (empty? s) + acc + (recur + (rest s) + (str (first s) acc))))) diff --git a/exercises/practice/reverse-string/.approaches/sequence/content.md b/exercises/practice/reverse-string/.approaches/sequence/content.md new file mode 100644 index 000000000..84e39bebc --- /dev/null +++ b/exercises/practice/reverse-string/.approaches/sequence/content.md @@ -0,0 +1,61 @@ +# It's a sequence + +```clojure +(defn reverse-string [string] + (apply str (reverse string))) +``` + +In Clojure, many functions that operate on sequences will automaticaly convert a string parameter into a sequence of characters. +This is because many core functions call `seq` on its arguments. +Also, ["most of Clojure's core library treats collections and sequences the same way"][collections-and-sequences]. +It follows that we can use any method to reverse a sequence or a collection to reverse a string. + +There will be three stops in this group of approaches: + +1. Convert a string to a sequence or a collection. (Is usually implicit, part of the next step). +2. Reverse the sequence or a collection. +3. Convert the sequence of characters back into a string. (It has to be explicit). + +## Reversing + +Here are a few options for reversing a sequence of characters. +```clojure +(reverse s) +``` +The above is self-explanatory. +```clojure +(into () s) +``` +This takes one character at a time from `s` and adds it to a sequence. +Because in Clojure, by default, new elements are added to the beginning of the list, +`into` reverses the characters at the same time as changing a string into an explicit sequence. + +The more explicit verbose version of this operation could be something like this: +```clojure +(reduce conj () s) +``` + +## Converting back to a string + +There are many options here, too. + +```clojure +(apply str (reverse s)) +(reduce str (reverse s)) +(clojure.string/join (reverse s)) +``` + +## A single step version + +We also have an option to combine all three operations into a single function call: + +```clojure +(defn reverse-string [s] + (reduce #(str %2 %1) "" s)) +``` + +## Which one to use? + +I'd suggest using the one that is the most readable to you. + +[collections-and-sequences]: https://clojure-doc.org/articles/language/collections_and_sequences/ \ No newline at end of file diff --git a/exercises/practice/reverse-string/.approaches/sequence/snippet.txt b/exercises/practice/reverse-string/.approaches/sequence/snippet.txt new file mode 100644 index 000000000..b39884d67 --- /dev/null +++ b/exercises/practice/reverse-string/.approaches/sequence/snippet.txt @@ -0,0 +1,2 @@ +(defn reverse-string [string] + (apply str (reverse string))) diff --git a/exercises/practice/reverse-string/.approaches/string-builder/content.md b/exercises/practice/reverse-string/.approaches/string-builder/content.md new file mode 100644 index 000000000..6c3df76e1 --- /dev/null +++ b/exercises/practice/reverse-string/.approaches/string-builder/content.md @@ -0,0 +1,46 @@ +# String builder + +```clojure +(defn reverse-string [s] + (.toString + (.reverse + (StringBuilder. s)))) +``` + +In Clojure, as in Java, strings are immutable. +It means that with every change we want to make to any string, we create a new string in memory. + +String recreation can be very resource-intensive, especially when new strings are created in many steps. +This is a common problem, so Java provides the `StringBuilder` class, which holds characters as a collection, +allowing for modifications until we are ready to create the string by calling `toString()` method. + +String builder has a built-in `reverse()` method, which we can use as shown above. +The complete approach is to initialise a `StringBuilder` with the string we want to reverse. +Then reverse it, and finally convert it back to string. + +## The shortcut + +Is accessing the underlying Java Virtual Machine and Java classes necessary? +Couldn't we just use the `clojure.string/reverse` function instead? + +We could! This is the alternative way to reverse a string: + +```clojure +(defn reverse-string [s] + (clojure.string/reverse s)) +``` + +In fact, at some level, we could consider both approaches to be equivalent. +Let's have a look at [the implementation of the `clojure.string/reverse`][implementation] function. + +```clojure +(defn ^String reverse + "Returns s with its characters reversed." + {:added "1.2"} + [^CharSequence s] + (.toString (.reverse (StringBuilder. s)))) +``` + +While there is a little bit more going on in the syntax, at its core, it is the same as the code at the top of this approach. + +[implementation]: https://github.com/clojure/clojure/blob/08a2d9bdd013143a87e50fa82e9740e2ab4ee2c2/src/clj/clojure/string.clj#L48C3-L48C3 diff --git a/exercises/practice/reverse-string/.approaches/string-builder/snippet.txt b/exercises/practice/reverse-string/.approaches/string-builder/snippet.txt new file mode 100644 index 000000000..e98cefb9f --- /dev/null +++ b/exercises/practice/reverse-string/.approaches/string-builder/snippet.txt @@ -0,0 +1,4 @@ +(defn reverse-string [s] + (.toString + (.reverse + (StringBuilder. s))))