3

I'm trying to store a closure as a HashMap value. If I pass the closure arg by value, everything works great:

use std::collections::hash_map::HashMap;

fn main() {
    let mut cmds: HashMap<String, Box<FnMut(String)->()>>
        = HashMap::new();

    cmds.insert("ping".to_string(), Box::new(|&mut:s| { println!("{}", s); }));

    match cmds.get_mut("ping") {
        Some(f) => f("pong".to_string()),
        _ => ()
    }
}

(playpen)

But if I want a closure that takes a reference arg, things go south:

use std::collections::hash_map::HashMap;

fn main() {
    let mut cmds: HashMap<String, Box<FnMut(&str)->()>>
        = HashMap::new();

    cmds.insert("ping".to_string(), Box::new(|&mut:s| { println!("{}", s); }));

    match cmds.get_mut("ping") {
        Some(f) => f("pong"),
        _ => ()
    }
}


<anon>:8:37: 8:78 error: type mismatch: the type `closure[<anon>:8:46: 8:77]` implements the trait `core::ops::FnMut(_)`, but the trait `for<'r> core::ops::FnMut(&'r str)` is required (expected concrete lifetime, found bound lifetime parameter )
<anon>:8     cmds.insert("ping".to_string(), Box::new(|&mut:s| { println!("{}", s); }));
                                             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<anon>:8:37: 8:78 note: required for the cast to the object type `for<'r> core::ops::FnMut(&'r str)`
<anon>:8     cmds.insert("ping".to_string(), Box::new(|&mut:s| { println!("{}", s); }));
                                             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
error: aborting due to previous error

(playpen)

I read the answer to How to rewrite code to new unboxed closures, and tried breaking out the map building into its own function in order to have a place to hang the where clause, but no dice:

use std::collections::hash_map::HashMap;

fn mk_map<F>() -> HashMap<String, (String, Box<F>)>
    where F: for<'a> FnMut(&'a str) -> ()
{
    let mut cmds: HashMap<String, (String, Box<F>)> = HashMap::new();
    cmds.insert("ping".to_string(), ("ping".to_string(), Box::new(|&mut:s| { println!("{}", s); })));
    cmds
}   

fn main() {
    let cmds = mk_map();
    match cmds.get_mut("ping") {
        Some(&mut (_, ref mut f)) => f("pong"),
        _ => println!("invalid command")
    }
}


<anon>:8:58: 8:99 error: mismatched types: expected `Box<F>`, found `Box<closure[<anon>:8:67: 8:98]>` (expected type parameter, found closure)
<anon>:8     cmds.insert("ping".to_string(), ("ping".to_string(), Box::new(|&mut:s| { println!("{}", s); })));
                                                                  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

(playpen)

What's the right way to do this?

Community
  • 1
  • 1
jfager
  • 279
  • 1
  • 8

1 Answers1

3

My solution:

#![allow(unstable)]
use std::collections::hash_map::HashMap;

// #1 returning a trait object   
fn mk_map<'a>() -> HashMap<String, (String, Box<FnMut(&str) + 'a>)> {
    let mut cmds : HashMap<_, (_, Box<FnMut(&str)>)> = HashMap::new();

    cmds.insert("ping".to_string(), ("ping".to_string(), 
        Box::new(|&mut: s: &str| { println!("{}", s); })));
    // #2                  ^-- give a little help to the compiler here
    cmds
}   

fn main() {
    let mut cmds = mk_map();
    // minor change: cmds needs to be mutable
    match cmds.get_mut("ping") {
        Some(&mut (_, ref mut f)) => f("pong"),
        _ => println!("invalid command")
    }
}

Ingredients:

  1. return a trait object
  2. give some help to the compiler on the type of the closure's parameter: Box::new(|&mut: s: &str|

To be honest, I'm not 100% sure about the reason for #2 (I mean, at least leaving it out should give a more intelligible error message). Probably an issue with rustc.

On #1, I'm almost sure it's required because you can't name a concrete return type for a closure returned from a function (it's an anonymous type created on the fly by the compiler), so Trait objects for now should be the only way to return a closure.

Appendix responding to comment:

imagine you have a trait Foo {} implemented by a few types:

trait Foo {}
impl Foo for u32 {}
impl Foo for Vec<f32> {}

if you write a function like you did with mk_map (let's call it make_foo) I commented that it's going to be hard to implement it. Let's see:

fn mk_foo<F>() -> Box<F> where F: Foo {
    unimplemented!()
}

the signature of mk_foo says that I should be able to call the function with any type that implements Foo. So this should all be valid:

   let a: Box<Vec<f32>> = mk_foo::<Vec<f32>>();
   let b: Box<u32> = mk_foo::<u32>();

i.e. the function, as written, is not returning a trait object. It's promising to return a Box with any concrete type the caller chooses. That's why it's not easy to actually implement the function. It should know how to create several types from nothing.

Paolo Falabella
  • 24,914
  • 3
  • 72
  • 86
  • 1
    Just the type hint on the closure arg seems to do it for the first-attempt code: http://is.gd/RU3bL1 – jfager Jan 12 '15 at 18:26
  • @jfager in the first attempt you are doing the same thing: inserting a trait Object in the hashmap. – Paolo Falabella Jan 12 '15 at 18:39
  • Not sure I follow what you mean. I was always trying to insert a trait object, that's what the Box was for. The fact that the closure arg needed the type hint wasn't obvious, like you said. – jfager Jan 12 '15 at 18:47
  • @jfager in your attempt with mk_map, your signature says that for any concrete type F implementing the trait in your where clause, the function will be able to return a Box with that concrete type. This is different from a trait object and a very hard signature to fulfill. – Paolo Falabella Jan 12 '15 at 20:15
  • Ah, ok, I don't actually care about mk_map, it was a half-hearted attempt to work around the actual issue. I don't fully understand why it's a difficult signature to fill, though - it's a concrete type, but it isn't a *named* concrete type, which is what I understood to be the thing you couldn't do w/ closures b/c the types were one-offs. Thanks. – jfager Jan 13 '15 at 00:20
  • @jfager I tried to address your comment directly in my answer, as it was hard to do in a comment – Paolo Falabella Jan 13 '15 at 13:12