Search code examples
stringrustreferencelifetimemutable

How to pass &mut str and change the original mut str without a return?


I'm learning Rust from the Book and I was tackling the exercises at the end of chapter 8, but I'm hitting a wall with the one about converting words into Pig Latin. I wanted to see specifically if I could pass a &mut String to a function that takes a &mut str (to also accept slices) and modify the referenced string inside it so the changes are reflected back outside without the need of a return, like in C with a char **.

I'm not quite sure if I'm just messing up the syntax or if it's more complicated than it sounds due to Rust's strict rules, which I have yet to fully grasp. For the lifetime errors inside to_pig_latin() I remember reading something that explained how to properly handle the situation but right now I can't find it, so if you could also point it out for me it would be very appreciated.

Also what do you think of the way I handled the chars and indexing inside strings?

use std::io::{self, Write};

fn main() {
    let v = vec![
        String::from("kaka"),
        String::from("Apple"),
        String::from("everett"),
        String::from("Robin"),
    ];

    for s in &v {
        // cannot borrow `s` as mutable, as it is not declared as mutable
        // cannot borrow data in a `&` reference as mutable
        to_pig_latin(&mut s);
    }

    for (i, s) in v.iter().enumerate() {
        print!("{}", s);

        if i < v.len() - 1 {
            print!(", ");
        }
    }

    io::stdout().flush().unwrap();
}

fn to_pig_latin(mut s: &mut str) {
    let first = s.chars().nth(0).unwrap();
    let mut pig;

    if "aeiouAEIOU".contains(first) {
        pig = format!("{}-{}", s, "hay");
        s = &mut pig[..]; // `pig` does not live long enough
    } else {
        let mut word = String::new();

        for (i, c) in s.char_indices() {
            if i != 0 {
                word.push(c);
            }
        }

        pig = format!("{}-{}{}", word, first.to_lowercase(), "ay");
        s = &mut pig[..]; // `pig` does not live long enough
    }
}

Edit: here's the fixed code with the suggestions from below.

fn main() {
    // added mut
    let mut v = vec![
        String::from("kaka"),
        String::from("Apple"),
        String::from("everett"),
        String::from("Robin"),
    ];

    // added mut
    for mut s in &mut v {
        to_pig_latin(&mut s);
    }

    for (i, s) in v.iter().enumerate() {
        print!("{}", s);

        if i < v.len() - 1 {
            print!(", ");
        }
    }

    println!();
}

// converted into &mut String
fn to_pig_latin(s: &mut String) {
    let first = s.chars().nth(0).unwrap();

    if "aeiouAEIOU".contains(first) {
        s.push_str("-hay");
    } else {
        // added code to make the new first letter uppercase
        let second = s.chars().nth(1).unwrap();

        *s = format!(
            "{}{}-{}ay",
            second.to_uppercase(),
            // the slice starts at the third char of the string, as if &s[2..]
            &s[first.len_utf8() * 2..],
            first.to_lowercase()
        );
    }
}

Solution

  • I'm not quite sure if I'm just messing up the syntax or if it's more complicated than it sounds due to Rust's strict rules, which I have yet to fully grasp. For the lifetime errors inside to_pig_latin() I remember reading something that explained how to properly handle the situation but right now I can't find it, so if you could also point it out for me it would be very appreciated.

    What you're trying to do can't work: with a mutable reference you can update the referee in-place, but this is extremely limited here:

    • a &mut str can't change length or anything of that matter
    • a &mut str is still just a reference, the memory has to live somewhere, here you're creating new Strings inside your function then trying to use these as the new backing buffers for the reference, which as the compiler tells you doesn't work: the String will be deallocated at the end of the function

    What you could do is take an &mut String, that lets you modify the owned string itself in-place, which is much more flexible. And, in fact, corresponds exactly to your request: an &mut str corresponds to a char*, it's a pointer to a place in memory.

    A String is also a pointer, so an &mut String is a double-pointer to a zone in memory.

    So something like this:

    fn to_pig_latin(s: &mut String) {
        let first = s.chars().nth(0).unwrap();
        if "aeiouAEIOU".contains(first) {
            *s = format!("{}-{}", s, "hay");
        } else {
            let mut word = String::new();
    
            for (i, c) in s.char_indices() {
                if i != 0 {
                    word.push(c);
                }
            }
    
            *s = format!("{}-{}{}", word, first.to_lowercase(), "ay");
        }
    }
    

    You can also likely avoid some of the complete string allocations by using somewhat finer methods e.g.

    fn to_pig_latin(s: &mut String) {
        let first = s.chars().nth(0).unwrap();
        if "aeiouAEIOU".contains(first) {
            s.push_str("-hay")
        } else {
            s.replace_range(first.len_utf8().., "");
            write!(s, "-{}ay", first.to_lowercase()).unwrap();
        }
    }
    

    although the replace_range + write! is not very readable and not super likely to be much of a gain, so that might as well be a format!, something along the lines of:

    fn to_pig_latin(s: &mut String) {
        let first = s.chars().nth(0).unwrap();
        if "aeiouAEIOU".contains(first) {
            s.push_str("-hay")
        } else {
            *s = format!("{}-{}ay", &s[first.len_utf8()..], first.to_lowercase());
        }
    }