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

1

10

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()
        );
    }
}
Ike answered 28/7, 2020 at 10:18 Comment(2)
There are multiple unrelated errors in the code, one may start by reading the compiler's error messages and fixing them as hinted by the compiler. The first error occurred because the vector v was declared as immutable (add a mut). Then, recommended reading: What are the differences between Rust's String and str?. It is very likely that you want a &mut String here. The rest of the issues seem to come from a mix-up between assigning variables and assigning the values behind the references, see: stackoverflow.com/questions/28587698Marieann
@E_net4isaflag I actually declared v as mut at the beginning but I guess because of the other errors the compiler gave me a warning that mutability wasn't necessary so I changed it without giving much thought. I have already read some explanations on Rust's quirks about strings but there might always be room for improvement. Lastly, I wanted to pass a &mut str to include slices but it seems that by doing so I can't avoid a return like I wanted.Ike
G
16

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());
    }
}
Granjon answered 28/7, 2020 at 10:57 Comment(3)
Yeah I didn't realize that &mut str would hold those pitfalls. The format! with slice in the last example is also very cool.Ike
I am struggling to find reference to the line that "String is also a pointer"; do you mean String is a smart pointer here? Or do you mean String is a pointer in general sense, e.g., a pointer to heap data? Thank you.Urease
In this case the second to show that that works as in C. It is also the first but that’s not germane to my answer.Granjon

© 2022 - 2024 — McMap. All rights reserved.