Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reverse string approaches #609

Merged
merged 12 commits into from
Jan 23, 2024
36 changes: 36 additions & 0 deletions exercises/practice/reverse-string/.approaches/config.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
]
}
53 changes: 53 additions & 0 deletions exercises/practice/reverse-string/.approaches/introduction.md
Original file line number Diff line number Diff line change
@@ -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
28 changes: 28 additions & 0 deletions exercises/practice/reverse-string/.approaches/recursion/content.md
Original file line number Diff line number Diff line change
@@ -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 in these two examples.
michalporeba marked this conversation as resolved.
Show resolved Hide resolved
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 examples above will be inefficient.
michalporeba marked this conversation as resolved.
Show resolved Hide resolved
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)))))))
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
(defn reverse-string [s]
(loop [s s acc ""]
(if (empty? s)
acc
(recur
(rest s)
(str (first s) acc)))))
61 changes: 61 additions & 0 deletions exercises/practice/reverse-string/.approaches/sequence/content.md
Original file line number Diff line number Diff line change
@@ -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/
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
(defn reverse-string [string]
(apply str (reverse string)))
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
(defn reverse-string [s]
(.toString
(.reverse
(StringBuilder. s))))
Loading