I create a HashMap which maps strings to functions of type Vec<Expression> -> Expression
, where Expression
is a type I have defined. The code in question is:
let functions: HashMap<_, _> = vec!(("+", Box::new(plus))).into_iter().collect();
If I let Rust infer the type for me, as in the code above, it compiles and runs fine, as in the code above. However, if I try to specify the type, it doesn't compile:
let functions: HashMap<&str, Box<Fn(Vec<Expression>) -> Expression>> =
vec!(("+", Box::new(plus))).into_iter().collect();
The compiler error message isn't very helpful:
let functions: HashMap<&str, Box<Fn(Vec<Expression>) -> Expression>> = vec!(("+", Box::new(plus))).into_iter().collect();
^^^^^^^ a collection of type `std::collections::HashMap<&str, std::boxed::Box<std::ops::Fn(std::vec::Vec<Expression>) -> Expression>>` cannot be built from an iterator over elements of type `(&str, std::boxed::Box<fn(std::vec::Vec<Expression>) -> Expression {plus}>)`
What is the actual type of this HashMap
?
If you look closely at the difference you will have your answer, although it can be puzzling.
I expect that plus
has been declared as:
fn plus(v: Vec<Expression>) -> Expression;
In this case, the type of plus
is fn(Vec<Expression>) -> Expression {plus}
, and is actually a Voldemort Type: it cannot be named.
Most notably, it differs from an eventual fn(Vec<Expression>) -> Expression {multiply}
.
Those two types can be coerced into a bare fn(Vec<Expression>) -> Expression
(without the {plus}
/{multiply}
denomination).
And this latter type can be transformed into a Fn(Vec<Expression>) -> Expression
, which is a trait for any callable which do not modify their environments (such as the closure |v: Vec<Expression>| v[0].clone()
).
The problem, however, is that while fn(a) -> b {plus}
can be transformed into fn(a) -> b
which can be transformed into Fn(a) -> b
... the transformation requires a change of memory representation. This is because:
fn(a) -> b {plus}
is a zero-sized type,
fn(a) -> b
is a pointer to function,
Box<Fn(a) -> b>
is a boxed trait object which generally means both a virtual pointer and a data pointer.
And therefore the type ascription doesn't work, because it can only perform cost-free coercions.
The solution is to perform the transformation before it's too late:
// Not strictly necessary, but it does make code shorter.
type FnExpr = Box<Fn(Vec<Expression>) -> Expression>;
let functions: HashMap<_, _> =
vec!(("+", Box::new(plus) as FnExpr)).into_iter().collect();
^~~~~~~~~~~~~~~~~~~~~~~~
Or maybe you'd rather keep unboxed functions:
// Simple functions only
type FnExpr = fn(Vec<Expression>) -> Expression;
let functions: HashMap<_, _> =
vec!(("+", plus as FnExpr)).into_iter().collect();
The relevant parts of error message are Box<std::ops::Fn ... >
and Box<fn ... {plus}>
. The first is a boxed Fn
trait object. The second is a boxed function plus
. Note that it isn't a boxed pointer to a function, which would be Box<fn ...>
with no {plus}
part. It is the unique and unnameable type of the function plus
itself.
That is you cannot write real type of this HashMap
, as the type it contains is unnameable. It's not a big deal though, you can only put plus
function into it.
The following code gives compilation error
let functions: HashMap<_, _> =
vec![("+", Box::new(plus)),
("-", Box::new(minus))].into_iter().collect();
^^^^^ expected fn item, found a different fn item
This works, but it is useless
let functions: HashMap<_, _> =
vec![("+", Box::new(plus)),
("-", Box::new(plus))].into_iter().collect();
One possible solution is to convert first element of a vector into the required type.
type BoxedFn = Box<Fn(Vec<Expression>) -> Expression>;
let functions: HashMap<&str, BoxedFn> =
vec![("+", Box::new(plus) as BoxedFn),
("_", Box::new(minus))].into_iter().collect();
Another one is type ascription of intermediate variable.
type BoxedFn = Box<Fn(Vec<Expression>) -> Expression>;
let v: Vec<(_, BoxedFn)> = vec![("+", Box::new(plus)), ("_", Box::new(minus))];
let functions: HashMap<&str, BoxedFn> = v.into_iter().collect();