Reputation: 1751
I'm writing a mocking framework. To do so, I need to accept a function which can be used as a replacement of another function and store it. My current design does that by forcing the same input and output types, but it fails completely at forcing correct lifetimes.
Instead, I need to write a generic function, which accepts a base function and its substitute:
fn main() {
selector(foo, baz, "local", false);
selector(bar, baz, "local", false);
selector(foo, bar, "local", false); // SHOULD FAIL, bar is NOT substitute of foo
}
fn foo(_: &str) -> &'static str {
"foo"
}
fn bar(s: &str) -> &str {
s
}
fn baz(_: &str) -> &'static str {
"baz"
}
// DOES NOT COMPILE
// fn selector<U, V, F: Fn(U) -> V, G: F>(base: F, subs: G, arg: U, use_base: bool) -> V {
// match use_base {
// true => base(arg),
// false => subs(arg),
// }
// }
// COMPILES, but is too weak
fn selector<U, V, F: Fn(U) -> V, G: Fn(U) -> V>(base: F, subs: G, arg: U, use_base: bool) -> V {
match use_base {
true => base(arg),
false => subs(arg),
}
}
By "substitute", I mean a function which accepts at least every argument accepted by base and for each of them returns value at least usable in every place, where value returned from base are. For example:
fn foo(_: &str) -> &'static str {
"foo"
}
fn bar(s: &str) -> &str {
s
}
fn baz(_: &str) -> &'static str {
"baz"
}
baz
is a substitute of foo
and bar
because its returned string can be used either instead of a 'static
one or be dependent on a borrow. bar
is not a substitute of foo
because a borrowed value can't be used in place of a 'static
one.
I want to create something like this, but it doesn't compile:
// FAILURE
// V
fn selector<U, V, F: Fn(U) -> V, G: F>(base: F, subs: G) {...}
The problem is that I can't express the relation between F
and G
. Rust does not seem to have a notion of supertype of concrete type.
Upvotes: 3
Views: 197
Reputation: 88536
Rust also knows about these kinds of "compatibility" of types: it's all about subtyping and variance. The trick is to simply make both arguments have the same type, at least as far as the function is concerned.
Let's try something easier first:
// Both arguments have the same type (including the same lifetime)
fn foo<'a>(x: &'a i32, y: &'a i32) -> &'a i32 {
x
}
let outer = 3;
{
let inner = 27;
println!("{}", foo(&outer, &inner));
}
Why does this work? outer
and inner
clearly have different lifetimes! Because when calling the function, Rust considers subtyping to see if the function can be called. In particular, the type &'outer i32
is a subtype of &'inner i32
so it can be turned into a &'inner i32
without a problem and the function can be called.
Applying this idea to your problem means that your function only has one function type and both arguments (base
and subs
) have that type:
fn selector<U, V, F: Fn(U) -> V>(base: F, subs: F, arg: U, use_base: bool) -> V {
match use_base {
true => base(arg),
false => subs(arg),
}
}
If we try it like that, we unfortunately still get an error: "expected fn item, found a different fn item". This has to do with the "function item vs. function pointer" problem. You can learn more about that in this answer or in the Rust reference. The coercion to function pointer is sadly not kicking in in this situation, so one way would be to cast the functions explicitly:
type FnStatic = fn(&str) -> &'static str;
type FnWeaker = fn(&str) -> &str;
selector(foo as FnStatic, baz as FnStatic, "local", false);
selector(bar as FnWeaker, baz as FnStatic, "local", false);
selector(foo as FnStatic, bar as FnWeaker, "local", false);
This actually works as expected: the first two calls are fine, but the third errors with:
error[E0308]: mismatched types
--> src/main.rs:7:31
|
7 | selector(foo as FnStatic, bar as FnWeaker, "local", false);
| ^^^^^^^^^^^^^^^ expected concrete lifetime, found bound lifetime parameter
|
= note: expected type `for<'r> fn(&'r str) -> &str`
found type `for<'r> fn(&'r str) -> &'r str`
However, it's still a bit ugly to cast the function types explicitly at call site. Unfortunately, I haven't found a way to hide this. I tried writing a macro that enforced the coercion, but that didn't work for when the function pointer had different types (including the second example).
Upvotes: 4