Converting String to &str in map and returning

I just started experimenting with rust. I want to return a &str from a map, the problem is hard to explain in text so I added comments to the code to explain it better

pub type Values<'a> = Vec<(&'a str, &'a str)>;

let mut values: Values = vec![
("param1", "value1"),
("param2",  "value2"),
];

// I want to call encode function on all values
//conceptually something like this
//in place is better but would be fine with returning a new Values type.
//i was able to make this work by creating an intermediate Vec<(String, String)> but that doesn't feel right
values.iter_mut().map(|v| {
            let v0 = encode(v.0);
            let v1 = encode(v.1);
            *v = (v0.as_str(), v1.as_str());
        });

fn encode(s: &str) -> String {
    let mut res = String::new();
    //some logic
   res
}

This can't work. With references (sich as &str), you always need an owner. Asking the questions "who is going to be the owner of the strs you created here?", the only answer would be that there is no owner (after the call to the closure on map returns. Therein lies the problem.

The only kind of mutations you can do with a list of &'a str values is to change their order, remove or duplicate some of them, and/or split or cut them up into smaller parts/slices. You can not generate a new string and use that one (unless that new string is somehow owned in a place that lives longer than the list of &'a str references at hand). For such operations, it's generally better to just use owned String. To avoid the need to convert the original values into owned String, too, you could consider creating a new Vec<(String, String)> instead of mutating the existing one.

2 Likes

exactly what I thought, i was thinking may be there is a "magical" way to make it work :smiley:
I think the only solution here is to do something like

        let intermediate: Vec<(String, String)> = params
            .iter()
            .map(|&(k, v)| (encode(k), encode(v)))
            .collect();

        let res: Values = intermediate
            .iter()
            .map(|&(ref k, ref v)| (k.as_str(), v.as_str()))
            .collect();

Even the most magical way would still need an owner. The approach you list here works because intermediate can own the strings, and res can borrow from it.

If you absolutely want the mutation of the existing Vec, you could e. g. use something like an arena (e. g. Arena<String>) from typed_arena - Rust as an owner that lives far enough up the stack to be around for all the time you need access to your created strings.

1 Like

This magic is called garbage collector, and Rust doesn't have one.

&str is not a string itself. It's a substring or a view into some string stored elsewhere.

The reason

let mut values: Values = vec![
("param1", "value1"),
("param2",  "value2"),
];

works, but your proposed changes can't, is that string literals evaluate to &'static str references, which point to memory the language guarantees will be valid for the life of the program, and which will not change. There isn't really a way to generate new &'static str references at runtime that aren't derived from the original strings in some fairly direct way (substring, mainly), unless you're comfortable with memory leaks.

The way to make this work would be to change Values to own its data, instead:

pub type Values = Vec<(String, String)>;

let mut values = vec![
 ("param1".into(), "value1".into()),
 ("param2".into(), "value2".into()),
];

    values.iter_mut().for_each(|v| {
        let v0 = encode(&v.0);
        let v1 = encode(&v.1);
        *v = (v0, v1);
    });

You might notice that this also uses for_each, not map. map doesn't actually run the closure or iterate over the underlying collection until the iterator is consumed, and in your example, that never happens. for_each is eager, and consumes the iterator immediately.

Frankly, I would be inclined to write this so that it doesn't modify values in place, because it's easier to reason about, but this is valid.

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.