Generics and type confusion

I'm trying to make a function that accepts an AsRef trait to make it easier for myself to call it with either a String or a &str or basically anything on that form. But I have a problem with the compiler telling me I have the wrong type.

In the following code the compiler is giving me an error.

fn caller<T: AsRef<str>>(data: &[(T, T)], chars: &Option<T>) -> String {
    let strings: Vec<&str> = data.iter().map(|t| t.0.as_ref()).collect();
    receiver(&strings, chars)
}

fn receiver<T: AsRef<str>>(strings: &[T], chars: &Option<T>) -> String {
    return String::new();
}
error[E0308]: mismatched types
  --> src\main.rs:31:24
   |
29 | fn caller<T: AsRef<str>>(data: &[(T, T)], chars: &Option<T>) -> String { 
   |           - this type parameter
30 |     let strings: Vec<&str> = data.iter().map(|t| t.0.as_ref()).collect();
31 |     receiver(&strings, chars)
   |                        ^^^^^ expected `&str`, found type parameter `T`   
   |
   = note: expected reference `&std::option::Option<&str>`
              found reference `&std::option::Option<T>`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.

But if I make the calling simpler, and remove the extra parameter and only keep the chars part, it works without issue.

fn caller2<T: AsRef<str>>(chars: &Option<T>) -> String {
    receiver2(chars)
}

fn receiver2<T: AsRef<str>>(chars: &Option<T>) -> String {
    return String::new();
}

What is going on here in the compiler? I know it's something to do with the strings conversion I am doing, but I cannot wrap my head around it. Does it hardlock the type of T when I call the .as_ref() function in the mapping?

The problem is that receiver expects (strings: &[T], chars: &Option<T>) where both Ts have to be the same type. You could go with

fn receiver<T1: AsRef<str>, T2: AsRef<str>>(strings: &[T1], chars: &Option<T2>) -> String {
    return String::new();
}

instead.

Alternatively, you can change the call to no longer use &[&str], &Option<T>, but e.g. &[&str], &Option<&str> instead, by receiver(&strings, &chars.as_ref().map(T::as_ref))

Yet another option: Call it with &[&T], &Option<&T> by

fn caller<T: AsRef<str>>(data: &[(T, T)], chars: &Option<T>) -> String {
    let strings: Vec<&T> = data.iter().map(|t| &t.0).collect();
    receiver(&strings, &chars.as_ref())
}

Edit: And if you take the generics even further and use iterators, you can get rid of the intermediate Vec (playground with questionably over-generic code example).

6 Likes

For this sort of type signature, I often prefer impl syntax. It reduces the visual distance between the parameter name and the type it actually takes. This is equivalent to the code above, for instance:

fn receiver(strings: &[impl AsRef<str>], chars: &Option<impl AsRef<str>>) -> String {
    return String::new();
}
4 Likes

I do like the idea of this and will probably start doing exactly this for simpler function signatures.

In my case here though, the function is longer and the type generic has more constraints than just AsRef<str>

Thanks for the good explanation.

I'm coming from other languages were the type T is not restricted by what the function calling it might implement it as, so it threw me off and I didn't think of it, but it does make perfect sense.

Specifically which language allows a single type parameter to designate several unrelated types? That seems odd.

1 Like

Hmm... After looking a bit further into this, It's probably a misconception that I've held and only just now realized wasn't the case. So you were definitely right in it sounding wrong!

My thought was that it was just any parameter that satisfies the constraint, but not necessarily the same. I can see how that would pose certain types of problems and splitting them into two separate type parameters is definitely just something that I need to get used to in certain situations.