CodeSandwich
CodeSandwich

Reputation: 1751

Generic parameter bounded by another parameter

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),
    }
}

Playground

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

Answers (1)

Lukas Kalbertodt
Lukas Kalbertodt

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); 

(Playground)

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

Related Questions