A newbie Ownership Question with Rust book exercise

Trying to solve the problem from rust book

"Convert strings to pig latin. The first consonant of each word is moved to the end of the word and “ay” is added, so “first” becomes “irst-fay.” Words that start with a vowel have “hay” added to the end instead (“apple” becomes “apple-hay”). Keep in mind the details about UTF-8 encoding!"

Solution 1 using String which seem to work like a charm,

fn main() {
    let mut fruits: Vec<String> = vec![
        String::from("apple"),
        String::from("banana"),
        String::from("mango"),
        String::from("peach"),
        String::from("orange"),
    ];
    for s in fruits.iter_mut() {
        if let Some(c) = s.char_indices().next() {
            match c.1 {
                'a' | 'e' | 'i' | 'o' | 'u' => *s = format!("{}-hay", s),
                _ => {
                    let suffix = format!("-{}ay", c.1);
                    *s = format!("{}{}", &s[1..], suffix)
                }
            }
        }
    }
    println!("{:?}", fruits)
}

Just to understand the ownership concepts more, I tried to replace String with &str and wrote the following solution, it does not work

fn main() {
    let mut fruits: Vec<&str> = vec!["apple", "banana", "mango", "peach", "orange"];
    for s in fruits.iter_mut() {
        if let Some(c) = s.char_indices().next() {
            match c.1 {
// The following line gives error like temporary value dropped while borrowed, use let binding 
                'a' | 'e' | 'i' | 'o' | 'u' => *s = format!("{}-hay", s).as_str(),
                _ => {
                    let suffix = format!("-{}ay", c.1);
// The following line gives error like temporary value dropped while borrowed, use let binding 
                    *s = format!("{}{}", &s[1..], suffix).as_str();
                }
            }
        }
    }
    println!("{:?}", fruits)
}

I could understand this is coming from format, but not able to understand the error completely.

  1. Does this mean &str is not right for this solution ?
  2. The same thing works with String even that case the value is borrowed the same way right?

Thanks

Kamesh

Right, you can't store a reference to the String (&str) in the Vec when the String itself has been dropped. It is dropped because it is discarded (no longer used) in the expression using format!, after getting the reference. That's what the error message temporary value dropped while borrowed is telling you.

In the first case, which works, you're storing the String itself in the Vec. It is not borrowed, and is not dropped because the Vec still owns it.

2 Likes

thanks @jumpnbrownweasel,

It is dropped because it is discarded (no longer used) in the expression using format! , after getting the reference

this could technically lead to dangling reference and Rust prevents it.

Am I right to say , &str is not the right choice as T in Vec<T>, its better and best to allow the Vec to own its elements, matter of fact better for any collection to own its elements as then it has more control.

Yes, certainly in this case, since your task is to convert each String. You could instead create a new Vec of the transformed Strings, if you wanted to preserve the original Strings, but that's not necessary here given the problem statement.

matter of fact better for any collection to own its elements as then it has more control.

In general that is best, especially if you are new to Rust, but it is not the only possibility. Storing references in a collection is possible if the owned original objects (from which the references were borrowed) have a longer lifetime than the references. For example, perhaps for some reason you want to sort a shorter-lived Vec of &str references, while keeping the owned Strings in their original longer-lived Vec. Using references in that case would avoid having to clone the Strings, which would require allocating memory.

3 Likes

Why is it that we can’t dereference the &str and clone the value?

str is a dynamically-sized type, so it can't be passed around by value.

https://doc.rust-lang.org/reference/dynamically-sized-types.html

But slices can be cloned? Why can’t compiler see the vectors contents and use that to infer size of the &str?

Slices don't support cloning via the Clone trait. However they do support a variant of cloning, that is the ToOwned trait, which is like Clone but the return type can be a different type. Whilst clone method does fn(&T) -> T, this signature can never be achieved for a DST since you can't return them by value. to_owned is more general fn(&T) -> T::Owned where for types like str, the associated str::Owned is defined to be String, and for [T] we have <[T]>::Owned defined as Vec<T>. (For all types that do implement Clone, ToOwned is also implemented using T::Owned = T.)

Whilst return values like fn(…) -> str are impossible, with indirection, of course it can be returned. This is why the standard library also offers Clone implementations for types like Box<str> and Box<[T]>, and &T is Copy and Clone, too, for all types including dynamically sized ones. This results in clone methods with signatures such as fn(&Box<str>) -> Box<str> or fn(&&str) -> &str, the latter more specifically for<'a, 'b> fn(&'a &'b str) -> &'b str to specify the lifetimes.

2 Likes

No. I'm not sure what you are confusing that with, but no dynamically-sized type is cloneable.

Why aren't the lifetimes subtyped in this case? Do we know that 'b is correct because it's to the right of the first lifetime?

So according to fn(&&str) -> &str we can dereference indirections only up to the primitive value.

No, not at all. The declaration order of lifetimes doesn't affect whether they are equal or not (or subtypes of each other).

The fact that you use 'b in a type that is the referent of a reference &'a _ implicitly adds a 'b: 'a bound in order to satisfy well-formedness (because nothing else makes sense – &'a &'b T cannot possibly be valid unless b outlives a – the opposite would be the very definition of a dangling reference).

I don't know what this is supposed to mean, but this doesn't seem to have anything to do with whether something is a "primitive". You can dereference all references. But again, you can't move a dynamically-sized value (whether it's a built-in or a UDT).

1 Like

I was confusing the size of a type with the size of its value: "Cannot know size until runtime: str" - - Reddit - Dive into anything

There is no fn(&str) -> str where str is a primitive type.

It fits the generic type of Clone::clone, which is fn clone(&self) -> Self, which is short for fn clone<'a>(self: &'a Self) -> Self. If you substitute in the same type &'b str for both occurrences of “Self” you get that the &'b str: Clone implementation gives rise to for<'a> fn(&'a &'b str) -> &'b str. With &'b str: Clone for all lifetime 'b, one can thus claim that it’s (at least comparable to) a function for<'a, 'b> fn(&'a &'b str) -> &'b str.

TL;DR it’s the lifetime 'b because it must fit the pattern fn(&T) -> T of the Clone::clone method, with both T being the same type. So with T = &str the lifetime of the return type corresponds to the inner/rightmost lifetime of the type &T = &&str.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.