Enable compiler to auto-convert &str to String

How it currently works:

fn main() {
    let comma_separated_list = "first,second";
    let string_vec = comma_separated_list
        .split(',')
        .map(ToString::to_string) // this line should be inferred by compiler
        .collect::<Vec<String>>();

    assert_eq!(
        string_vec,
        vec![
            "first".to_string(),
            "second".to_string()
        ]
    );
}

How I think it should work:

fn main() {
    let comma_separated_list = "first,second";
    let string_vec = comma_separated_list
        .split(',')
        .collect::<Vec<String>>();

    assert_eq!(
        string_vec,
        vec![
            "first".to_string(),
            "second".to_string()
        ]
    );
}

The above example is probably just a concrete instance of something broader that's applicable to many other types and situations

Rust generally doesn't do implicit conversions and allocations.

There is no place where &str implicitly becomes a String (without something like impl Into<String> in an interface), so why should collect be the first (only) one place where this happens?

collect already goes through a pretty clever generic machinery of FromIterator. Addition of implicit conversion could make that ambiguous for type inference, or just too difficult to reason about.

.map(From::from) is shorter. .map(String::from) may be clearer.

15 Likes

To clarify your position (which I agree with as I understand it):

Even if the inference machinery magically worked (e.g. weird specialization of FromIterator for &strs to collect into a Vec<String>), the real issue is that the allocation for each string should be explicit, and not hidden inside the collect call.

If there's no place to point to for "you're allocating all of these strings via this conversion", that's in plain sight, then it can obscure unnecessary allocations. (the Vec allocation is in plain sight, on the other hand, as the one allocation done by collect())

6 Likes

Tangentially, I am pretty fond of collect_vec() from itertools for this sort of code: collect() is a brilliant piece of type-inference idiom which is too general for 80% of its uses.

1 Like

None of these versions hide allocations, the result is a Vec<String>, there are allocations. It's more about rephrasing the requested functionality to something concrete (the original post could be saying "collect should implicitly call From::from") and then wondering about the consequences of that change. One of the consequences is that the code is going to usually contain more explicit types than before (if I collect an iterator of u16, does it become a Vec<u16> or a Vec<u32>? Both make sense after calling from and there is no such thing as default types).

If the original ask is more narrow (this conversion should only happen for &strString), that's maybe just a confusing request. Possibly we want a new method .map_from_collect(), but that's really not that much shorter/clearer than .map(From::from).collect().

Side note: implicit allocation is fine (outside of kernels and friends). Lots of code implicitly allocates. Surprising allocation is not fine, but most surprising behavior of code is not fine.

2 Likes