Cannot pass P of generic<P>(HashMap<String,P>)

I want to pass a generic type P to HashMap<String, P) in the following function, but the compiler seems to interpret P as an empty struct instead of a generic type. Is this a bug, if not, is there a way to workaround it?

    use std::{collections::HashMap, sync::{Arc, RwLock}};

    /// Clear any prior entries
    pub fn clear_map<P>(parse_map: &Arc<RwLock<HashMap<String, P>>>) -> Result<(), ()>
    {
        {
            match parse_map.write() {
                Ok(mut map) => {
                    for (_key, entry) in map.iter_mut() {
                        entry.value.clear();
                    }
                }
                Err(e) => {
                    println!("clear_map failed to lock {}", format!("{}", e));
                    return Err(());
                }
            }
        }
        Ok(())
    }

    fn main() {
    }
   Compiling generic-test v0.1.0 (generic-test)
error[E0609]: no field `value` on type `&mut P`
  --> src\main.rs:10:27
   |
4  | pub fn clear_map<P>(parse_map: &Arc<RwLock<HashMap<String, P>>>) -> Result<(), ()>
   |                  - type parameter 'P' declared here
...
10 |                     entry.value.clear();
   |                           ^^^^^

error[E0609]: no field `value` on type `&mut P`
  --> src\main.rs:10:27
   |
4  | pub fn clear_map<P>(parse_map: &Arc<RwLock<HashMap<String, P>>>) -> Result<(), ()>
   |                  - type parameter 'P' declared here
...
10 |                     entry.value.clear();
   |                           ^^^^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0609`.
error: aborting due to previous error

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

Nope, there's no bug here.

You're telling the compiler you want a HashMap with String keys and values of generic type P.

Then you're telling it you want to get the field value of the entry of that type.

The compiler is refusing to comply, because you've never told it that a value of type P is supposed to have fields. You didn't tell it anything about values of types P at all. It has no clue what to do with them.

If you're planning to do anything with the things of type P that you're going to pass to your function, then you must first define a separate trait block to say that is going to define that behaviour.

trait HasClearableValue {
     fn clear();
}

Then you'll have to implement that behaviour for each and every type that you're planning to pass into your clear_map function:

struct MyStruct {
    value: MyType;
}

impl HasClearableValue for MyStruct {
    fn clear(&mut self) {
        self.value.clear();
    }
}

And then you'll have to tell your clear_map function to only accepts HashMaps with values that behave according to the trait you've just defined:

pub fn clear_map<P: HasClearableValue>(parse_map: &Arc<RwLock<HashMap<String, P>>>)

5 Likes

@Ideator Thanks for the detailed response, that all makes sense,

I wasn't sure how it was supposed to be able to compile without more knowledge about the generic type. I was thinking it would be more of a template and not compile until it had a concrete type, this now makes perfect sense!

FYI: I think your description would be good to add to the generics section of the docs, I didn't find anything so clearly worded after reading it multiple times.

(Given that traits can't have fields, I probably should have figured this out, I was trying to shortcut writing a lot of duplicate code where just the type gets changed. This is one place where something like a template would help, but I wouldn't want to open that can of worms as I have seen how gnarly they can get in C++.)

Thanks,
-Dave

This is one of the things I love about Rust. If a generic function has no type errors, it is guaranteed to work for any choice of type that matches the trait requirements.

2 Likes

You are confusing type checking with code generation.

Generic Rust code isn't compiled directly to LLVM or assembly until it's got concretized by specifying type parameters, this is true.

However, generating assembly isn't the part where errors are being found out. The compiler still type checks the entire body of a generic function upfront – that is perfectly possible given the trait bounds imposed upon its generic type parameters. However, no actual executable code is generated at this stage.

This means that once a generic function type checks, we're done. As long as the substituted concrete type parameters fulfill the required trait bounds, the function is guaranteed to work and "compile", ie. correctly-functioning LLVM or assembly can be generated from the same, already-typechecked "template" concretized by its type parameters. Furthermore, there can't be unpleasant surprises (like those around C++ templates), namely that you anticipated a function to compile when called in a certain way, but it doesn't, and you can't find this out when you write the function.


A simplified/trivial example might help: Playground.

This code doesn't compile, although it's clear that an i32 is printable and it implements Display. Why the error then?

If you look at the compiler error, it doesn't point to the invocation of the concretized print_me::<i32> function. It couldn't care less about invocations. It shows the error in the definition, because at function definition time, you didn't declare that the type T should be displayable.

1 Like