Search code examples
genericsrustclosureslifetime

Explicitly boxing lifetime-bound conversion closures in Rust


As part of a larger system, I've got a collection of strongly-typed functions that can be called back. Though due to the dynamic nature of that system, the actual arguments given to these functions are stored as Rc<dyn Any>. To keep the API as strongly typed as possible, the function arguments are all runtime convertible From<&'a dyn Any>. These function arguments have their lifetime bound to 'a (they can be thought as views, or simply exposing a reference to the underlying instance of the dyn Any).

When a user registers a new function, a closure doing the conversion from Rc<dyn Any> to the strongly typed arguments is automatically added for them. The code can be simplified and thought as the following:

use std::{any::Any, rc::Rc};

struct View<'a, T> {
    view: &'a [T],
}

struct Foo {
    bar: String,
}

impl<'a, T: 'static> From<&'a dyn Any> for View<'a, T> {
    fn from(value: &'a dyn Any) -> Self {
        Self {
            view: value.downcast_ref::<Vec<T>>().unwrap(),
        }
    }
}

impl<'a> From<&'a dyn Any> for &'a Foo {
    fn from(value: &'a dyn Any) -> Self {
        value.downcast_ref().unwrap()
    }
}

fn print_content(view: View<i32>, foo: &Foo) {
    println!("view: {:?}", view.view);
    println!("foo: {}", foo.bar);
}

fn create_function() -> Box<dyn Fn(&[Rc<dyn Any>])> {
    Box::new(|args: &[Rc<dyn Any>]| print_content(args[0].as_ref().into(), args[1].as_ref().into()))
}

fn main() {
    let a = Rc::new(vec![1, 2, 3]);
    let b = Rc::new(Foo {
        bar: "hello".into(),
    });

    let args = vec![a as Rc<dyn Any>, b];

    let f: Box<dyn Fn(&[Rc<dyn Any>])> = create_function();
    f(&args);
}

So far so good. With the print_content() method being explicitly given to the compiler, the lifetimes are correctly resolved, the code compiles and runs just fine, printing:

view: [1, 2, 3]
foo: hello

Things fall apart when I try to make a generic method taking the function as an argument:

fn create_function_with<'a, Function, A, B>(f: Function) -> Box<dyn Fn(&'a [Rc<dyn Any>])>
where
    Function: 'static + Fn(A, B),
    A: From<&'a dyn Any>,
    B: From<&'a dyn Any>,
{
    Box::new(move |args| f(args[0].as_ref().into(), args[1].as_ref().into()))
}

fn main() {
    let a = Rc::new(vec![1, 2, 3]);
    let b = Rc::new(Foo {
        bar: "hello".into(),
    });

    let args = vec![a as Rc<dyn Any>, b];

    let f: Box<dyn Fn(&[Rc<dyn Any>])> = create_function_with(print_content);
    f(&args);
}

The compilation error is:

error[E0308]: mismatched types
  --> src/main.rs:54:42
   |
54 |     let f: Box<dyn Fn(&[Rc<dyn Any>])> = create_function_with(print_content);
   |                                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ one type is more general than the other
   |
   = note: expected trait object `dyn for<'a> Fn(&'a [Rc<dyn Any>])`
              found trait object `dyn Fn(&[Rc<dyn Any>])`

hinting that the lifetime constraints of the create_function_with() are not quite returning a dyn Fn(&[Rc<dyn Any>]) that works independently of the lifetime of &[Rc]`.

I've tried many variations of the above, including using Higher-Rank Trait Bounds lifetimes. I don't know whether I've reached a limitation of my knowledge of Rust (likely) or a limitation of the language itself (less-likely).

In the meantime, I've managed to work around the problem using macros (effectively going back to the initial working version with the function name inlined in the conversion closure).

Is there some advanced lifetime constraints/definitions that I'm missing here?


Solution

  • Note that the real type of the return value of create_function is

        let f : Box<dyn for <'a> Fn(&'a [Rc<dyn Any>])> = create_function();
    

    Very roughly, due to lifetime elision, the references without explicit bounds in the return type become generalized by the nearest binder. This happens all the time in Rust, it lets you write things like fn print_content(view: View<i32>, foo: &Foo) instead of fn print_content<'a>(view: View<'a, i32>, foo: &'b Foo).

    This is the type of a function which takes a reference of any lifetime.

    How do the bounds on such strongly typed functions look like? Well you could try this:

    fn create_function_with<A, B, Function>(f: Function) -> Box<dyn for<'a> Fn(&'a [Rc<dyn Any>])>
    where
        Function: 'static + for <'x, 'y> Fn(View<'x, A>, &'y B)
    

    but this is not super useful to you because you have multiple different view types (potentially even more than these two) and they can occur in different argument positions. So you have to abstract even further - the generic types A and B should be type formers, and you should be able to constrain the formed types with From:

    fn create_function_with<A, B, Function>(f: Function)
    where
        Function: 'static + for <'x, 'y> Fn(A<'x>, B<'y>)
               where A<'x> : From<&'x dyn Any>, B<'x> : From<&'y dyn Any>
    

    Note that this is hypothetical syntax, it doesn't even parse. Here A and B are higher kinded types, which are not supported by Rust. You can simulate these, but it becomes hard to use and anti-compositional. At this point I would suggest the macro solution, but otherwise you can do it like this:

    trait ViewLike
    {
        type ViewT<'x>;
        fn from_dyn<'x>(value: &'x dyn Any) -> Self::ViewT<'x>;
    }
    
    impl<T : 'static> ViewLike for View<'static, T> {
        type ViewT<'x> = View<'x, T>;
        
        fn from_dyn<'x>(value: &'x dyn Any) -> View<'x, T>
        {
            View {
                view: value.downcast_ref::<Vec<T>>().unwrap(),
            }
        }
    }
    
    impl<T : 'static> ViewLike for &'static T {
        type ViewT<'x> = &'x T;
        
        fn from_dyn<'x>(value: &'x dyn Any) -> &'x T
        {
            value.downcast_ref::<T>().unwrap()
        }
    }
    

    Note that here the Self type of ViewLike is not important and we never use it as a type for a value. It's just a tag type. We need to refer to to <Something as ViewLike>::ViewT<'x> to get the real value type. We lose a lot of type inference here.

    fn create_function_with<Va, Vb, Function>(f: Function) -> Box<dyn for<'a> Fn(&'a [Rc<dyn Any>])>
    where
        Function: 'static + for <'x, 'y> Fn(<Va as ViewLike>::ViewT<'x>, <Vb as ViewLike>::ViewT<'y>),
        Va : ViewLike,
        Vb : ViewLike,
    {
        Box::new(move |args| f(
            <Va as ViewLike>::from_dyn(args[0].as_ref()),
            <Vb as ViewLike>::from_dyn(args[1].as_ref())))
    }
    
    fn main() {
        ...
        let f = create_function_with::<View<i32>, &Foo, _>(print_content);
        f(&args);
    }