Saturday, January 8, 2022

Duplicating a String in Rust

So you want to append a string to itself - that is, taking "Ling" and somehow coming up with "LingLing". Should be simple, right?

s = "Ling";

s = s + s;

Okay, let's try this in Rust:

let mut s = "Ling";

s = s + s;

Rust says nope: you need to borrow the string object, not transfer ownership! Okay, let's try this:

s = &s + &s;

Again, wrong! The first object's ownership must be transferred, as rustc helpfully advises:

help: String concatenation appends the string on the right to the string on the left and may require reallocation. This requires ownership of the string on the left

   |

63 |     s = s + &s;

   |         ~

Let's obey:

s = s + &s;

Again, nope: in the operation we are trying to use an object whose value has been moved already! 

Trying another strategy: use the push_str() method:

s.push_str(&s);

Fail again: we are already using 's' as immutable, so we cannot mutate (borrow it as a mutable).  OMG, do we need to copy then? Let's try:

let s2 = s.clone();
s = s + &s2;

Yes, that works. It is painfully inefficient though... How can we improve?

Idea 1: use format macro:

s = format!("{}{}", s, s);

...yes, that works but it still involves parsing the format string run-time. Too inefficient.

The best solution would be to have a String constructor that takes a bunch of strings and concatenates them. This does not exist, the closest to it is joining an array of strings:

s = ["one", "two"].concat();

Let's try to do this to do our duplication:

s = [&s, &s];

Again, nope! Compiler is less helpful here:
the following trait bounds were not satisfied:

           `[&String]: Concat<_>`

...this means that the Concat<_> trait must be implemented for string refs. So Rust does not see that this should be treated as slices? (Interestingly, elsewhere Rust can do deref coercion... if I'm right, meaning, it knows how to change &String to &str...). But oh well, let's use slices explicitly:

    s = [&s[..], &s[..]].concat();

Yes, works, but this is more than ugly!

Bottom line: if code clarity is important, let's stick with:

s = s.clone() + &s;

If performance is of essence, do: 

    let mut scopy = String::with_capacity(2 * s.len());
    scopy.push_str(&s);
    scopy.push_str(&s);

I expect this to be twice as fast as the clone() version, based on str concatenation measurements here: https://github.com/hoodie/concatenation_benchmarks-rs .