let words = ["alpha", "beta", "gamma"];
let merged: String = words.iter().flat_map(|s| s.chars()).collect();
assert_eq!(merged, "alphabetagamma");
I'd like to add a format! to the mix:
let merged: String =
words.iter().flat_map(|s| format!("{s}!").chars()).collect();
// error[E0515]: cannot return reference to temporary value
// |
// 19 | words.iter().flat_map(|s| format!("{s}!").chars()).collect();
// | ---------------^^^^^^^^
// | |
// | returns a reference to data owned by the current function
// | temporary value created here
// |
// = help: use `.collect()` to allocate the iterator
Thanks, mister compiler, |s| format!("{s}!").chars().collect::<Vec<_>>() indeed works, but at the cost of an additional allocation. On StackOverflow folks are discussing alternative approaches that help to avoid that. My question is:
Why not make the return value take the ownership? Maybe doing so implicitly is a bad idea, but what if move keyword can be reused for this purpose?
move - make a closure take ownership of all its captures
Will become:
move - make a closure take ownership of all its captures, make a return value take ownership of all its references owned by the current function
The functionality even sounds similar, doesn't it? Reads quite well in my opinion:
let merged: String =
words.iter().flat_map(|s| move format!("{s}!").chars()).collect();
fn foo() -> Chars {
let s = String::from("bar");
return move s.chars();
}
I haven't dived too deep into the compiler code yet and probably missing something, so, please, tell me why is this a terrible idea.
What would it mean for the return value to take ownership?
In the case of the flat_map, it expects a closure that returns an iterator, in this case you're returning the Chars iterator. Chars cannot have ownership of the string it iterates over (by it's definition).
You could work-around this by implementing your own char iterator that takes ownership of the source, that's just not what the std Chars does.
Also, it is complicated to make a return value that references a local or a temporary to also move that return value (it's a self-referential data type). your reference would point to the old location once you moved the data it borrows from
Even if you move the borrowed values with the return value that's still a move of the borrowed value, and thus invalidates the borrows.
You could make a point that in some situations (e.g. in this case with Chars and String) this doesn't constitute a problem, but:
you need to detect those situations and this is not trivial at all
even if you can detect them, you need to change the return type of the function since it won't be returning just a Chars anymore, but a Chars and a String, where the String has to be handled very carefully by the compiler. Again, not trivial at all.
This isn't possible, because in Rust types are used to manage memory. If you change memory management strategy, you change what type is it. A Chars that only borrows a &str is something else than Chars that destroys a String.
Every string created by format!() has to have a matching drop() call somewhere, and Rust uses types to track where that drop is supposed to happen.
This code requires first line to not drop the string in takes_chars (because doing so would destroy data in words), and the second line requires the same function to drop the string in Chars to free strings created in the second map.
So the Chars would have to implicitly become something like (Cow<str>, Chars) in everything that may possibly be connected to any closure using the return move feature. But such type would have a different size than regular Chars, which complicates a lot of things.