3

I'm relatively new to using Rust, and I'm using it for Advent of Code to help me learn. For the fourth problem, I want to create a lookup table using a HashMap to map from string keys to function values. I understand that Rust does not have syntactic sugar for creating HashMap literals so I am creating my HashMap from a slice. When I use fn function pointers it all works fine:

type ValidatorFn = fn(&str) -> bool;
...

    let validation_rules: HashMap<&str, ValidatorFn> = [
        ("byr", validate_birth_year as ValidatorFn), // "as" cast is necessary here...
        ("iyr", validate_issue_year),
        ("eyr", validate_expiration_year),
        ("hgt", validate_height),
        ("hcl", validate_hair_colour),
        ("ecl", validate_eye_colour),
        ("pid", validate_passport_id),
    ]
    .iter()
    .cloned()
    .collect();

However, this limits me to only being able to store functions defined with the fn keyword and not closures. As an exercise I would like to rewrite my code to use boxed Fn trait objects instead of fn pointers to allow the use of closures or functions. However, my naive attempt to do so does not work:

type ValidatorFn = Box<dyn Fn(&str) -> bool>;
...

    let validation_rules: HashMap<&str, ValidatorFn> = [
        ("byr", Box::new(validate_birth_year) as ValidatorFn),
        ("iyr", Box::new(validate_issue_year)),
        ("eyr", Box::new(validate_expiration_year)),
        ("hgt", Box::new(validate_height)),
        ("hcl", Box::new(validate_hair_colour)),
        ("ecl", Box::new(validate_eye_colour)),
        ("pid", Box::new(validate_passport_id)),
    ]
    .iter()
    .cloned()
    .collect();

Gives multiple compiler errors:

error[E0277]: the trait bound `dyn for<'r> Fn(&'r str) -> bool: Clone` is not satisfied
  --> src/main.rs:21:6
   |
21 |     .cloned()
   |      ^^^^^^ the trait `Clone` is not implemented for `dyn for<'r> Fn(&'r str) -> bool`
   |
   = note: required because of the requirements on the impl of `Clone` for `Box<dyn for<'r> Fn(&'r str) -> bool>`
   = note: required because it appears within the type `(&str, Box<dyn for<'r> Fn(&'r str) -> bool>)`
error[E0599]: no method named `collect` found for struct `Cloned<std::slice::Iter<'_, (&str, Box<dyn for<'r> Fn(&'r str) -> bool>)>>` in the current
 scope
   --> src/main.rs:22:6
    |
22  |     .collect();
    |      ^^^^^^^ method not found in `Cloned<std::slice::Iter<'_, (&str, Box<dyn for<'r> Fn(&'r str) -> bool>)>>`
    |
   ::: /Users/ryan/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/src/rust/library/core/src/iter/adapters/mod.rs:388:1
    |
388 | pub struct Cloned<I> {
    | -------------------- doesn't satisfy `_: Iterator`
    |
    = note: the method `collect` exists but the following trait bounds were not satisfied:
            `Cloned<std::slice::Iter<'_, (&str, Box<dyn for<'r> Fn(&'r str) -> bool>)>>: Iterator`
            which is required by `&mut Cloned<std::slice::Iter<'_, (&str, Box<dyn for<'r> Fn(&'r str) -> bool>)>>: Iterator`

Can someone help decipher this error message and let me know if what I'm trying to do is even possible? It seems to be telling me that either the Box or its contents cannot be cloned. I thought that a Box is basically just a pointer to somewhere on the heap though so I don't understand why that cannot be cloned?

Endzeit
  • 4,810
  • 5
  • 29
  • 52

1 Answers1

4

The correct way to build the hash map is to avoid the clone in the first place:

let validation_rules: HashMap<&str, ValidatorFn> = vec![
    ("byr", Box::new(validate_birth_year) as ValidatorFn),
    ...
]
    .into_iter()
    .collect();

The clone was necessary in your original code because you were iterating over references to items in the array, and clone() served as a convenient way to turn a reference into an actual object, by producing a fresh copy of the object behind the reference. Since the objects were fn which are themselves references to functions, no expensive cloning was taking place, just a pointer was copied from the array to the hashmap.

If you use into_iter(), you consume the original collection and iterate over actual values pulled out of it, so you don't need to clone them. Unfortunately into_iter() is not yet available for arrays, so you have to use a Vec or equivalent.

Finally, the question that remains is:

I thought that a Box is basically just a pointer to somewhere on the heap though so I don't understand why that cannot be cloned?

Box is not just a pointer, it is an owning pointer to a heap-allocated object. If you were to clone it just by copying the underlying pointer, as you suggest, dropping the clone and the original box would cause a double free. To safely clone a Box, the underlying object must also be cloned, which requires its type to implement Clone and some additional effort when the box contains a type-erased dyn Trait object.

The Rust smart pointer that implements cheap Clone by copying the pointer is called Rc and is safe because it uses a reference count to ensure that the object is dropped only when the last reference to it disappears.

user4815162342
  • 141,790
  • 18
  • 296
  • 355
  • I copied my original .iter().cloned() approach from the HashMap example docs: https://doc.rust-lang.org/std/collections/struct.HashMap.html#examples , would be helpful if they could also mention the vec / into_iter() approach there too. – Ryan Collingham Dec 05 '20 at 13:12
  • 1
    @RyanCollingham That's a good point, though the `vec![]` approach is probably not appropriate in the general docs, which have to be more stringent than a StackOverflow answer. The docs would need to warn that using a `vec![]` incurs allocation which is not always acceptable, and such warning would detract from the main message of how to construct a HashMap. The problem will go away once #65819 is resolved, and the docs will just use `[...].into_iter()` without needing to clone or allocate. – user4815162342 Dec 05 '20 at 13:29