Hashmap of String -> (Fn () -> ())

#1
  1. I have the following code:
pub fn s0() {}
pub fn s1() {}
pub fn s2() {}
pub fn s3() {}
pub fn s4() {}

pub fn fmain() {
    let mut ans = HashMap::<String, _>::new();
    ans.insert("s0", s0);
    ans.insert("s0", s1);
    ans.insert("s0", s2);
    ans.insert("s0", s3);
    ans.insert("s0", s4);
}
  1. This code is not compiling. I am expecting it to compile as all the funcs have type Fn() -> (). I suspect there’s some issue of the form “functions even if they have the same type, are treated as having different types”

  2. How do I solve this? (For this particular post, I prefer “full solution” to “hint”, as I think I’m misunderstanding multiple things right now.)

#2
  1. I think I can get this to work by

defining a new trait MyFuncT,
then creating an empty struct for each func
and attaching the function to the empty structs
and having the Hasmap be String -> MyFuncT

However, I would prefer to not have to create a struct for each func.

#3
  1. This seems to have worked, but I’m not sure why:
pub fn s0() {}
pub fn s1() {}
pub fn s2() {}
pub fn s3() {}
pub fn s4() {}

pub fn fmain() {
    let mut ans = HashMap::<String, &Fn()->()>::new();
    ans.insert("s0".to_string(), &s0);
    ans.insert("s1".to_string(), &s1);
    ans.insert("s2".to_string(), &s2);
    ans.insert("s3".to_string(), &s3);
    ans.insert("s4".to_string(), &s4);
}
#4

Yeah, and it can be simplified to

let mut ans = HashMap::<String, fn() -> ()>::new();
ans.insert("s1".into(), s0);

It’s because the definition is looking for a String and not a &'static str like "s1". Also, fn() -> () is a function pointer instead of a trait object like Fn() -> (), remember that they’re different.

3 Likes
#5

I think it’s something to do with type inference. IIRC each function has its own “function item type” that is unique to the specific function. So by default it’s probably inferring that you wanted a HashMap specifically of the fn() {s0} function item type, and therefore the first insert succeeds but all the others fail.

Now you can also coerce function items to function pointers, which are not unique to a specific function, and if you specify fn() -> () then it does work.

Or you specifically want Fn rather than fn then you need to do so via trait objects as in your revised example.

5 Likes
#6

Actually, that only happens for closures as well, where every closure can be its own type, but they can all be coerced into a Fn* type, but only some can be coerced into a fn() -> () type, as they are special and don’t capture anything

1 Like
#7

You sure? If you try to compile a proper version of the OP’s original code then you get the following error that refers to fn items:

   Compiling playground v0.0.1 (/playground)
error[E0308]: mismatched types
  --> src/main.rs:12:33
   |
12 |     ans.insert("s0".to_owned(), s1);
   |                                 ^^ expected fn item, found a different fn item
   |
   = note: expected type `fn() {s0}`
              found type `fn() {s1}`

error[E0308]: mismatched types
  --> src/main.rs:13:33
   |
13 |     ans.insert("s0".to_owned(), s2);
   |                                 ^^ expected fn item, found a different fn item
   |
   = note: expected type `fn() {s0}`
              found type `fn() {s2}`

error[E0308]: mismatched types
  --> src/main.rs:14:33
   |
14 |     ans.insert("s0".to_owned(), s3);
   |                                 ^^ expected fn item, found a different fn item
   |
   = note: expected type `fn() {s0}`
              found type `fn() {s3}`

error[E0308]: mismatched types
  --> src/main.rs:15:33
   |
15 |     ans.insert("s0".to_owned(), s4);
   |                                 ^^ expected fn item, found a different fn item
   |
   = note: expected type `fn() {s0}`
              found type `fn() {s4}`

error: aborting due to 4 previous errors

For more information about this error, try `rustc --explain E0308`.
error: Could not compile `playground`.

To learn more, run the command again with --verbose.
3 Likes
#8

Huh, I could’ve sworn that there wasn’t a per-fn type, but okay. Well thanks for the heads up; I will keep that in mind, next time I’m defining an fn based type.

1 Like
#9

@FenrirWolf is correct, each function is its own type, and they can be coerced into the general fn type

2 Likes
#10

The per-fn types have zero size, whereas a general fn() -> () type has to pass a pointer to the actual function, and the most general &dyn Fn() -> () needs two pointers for the capture environment and the vtable to call it.

7 Likes
#11

To expand on the previous answers with a concrete “solution” for the issue, what’s happening is type inference is using the “per-fn” type as the second type parameter, so on the second insert, it fails to typecheck, since each function has a different type.

What you can do to get around this is to explicitly define the function type as being a Box of Fn()->() values. Fn()->() is a trait implemented by void functions, which means that any function that takes no parameters and returns nothing satisfies this trait (compiler guarantees this). Here’s a playground with an example:

The reason you need a box is that some implementations of Fn()->() would have different sizes. The reason for this has to do with how the compiler ensures functions and closures satisfy these traits: It creates an “anonymous struct” that “captures” any values that are referenced by the closure. Different concrete functions might have different sizes based on how many values they capture and the size of those values. So to get around this constraint, we box each of the functions, and instead store a pointer to each function in our hash map.

2 Likes
#12

But because we’re just referencing fn foo() -> () objects, and not closures, we don’t have to include the Box<Fn() -> ()> because there is an easier solution of just using fn() -> () pointers which are smaller and are faster; given that they are the size of a standard system pointer (Same size as usize) and they only use a single dereference and the don’t have the drawbacks of dynamic dispatch (dyn T as in Box<dyn Fn() -> ()>) runtime overhead. On the other hand, if we were to use a capturing or moving closure, then yeah, we would end up needing to use a dyn Fn() -> () type within a box.

3 Likes
#13

This is a good point to remember that Fn()->() is a trait, not a type. They have a type that implements that trait, but they have different types.

So the answer is the same as usual for storing hetrogeneous types: store Box<dyn Fn()->()>, or make an enum.

3 Likes