Customized trait object in Rust

I love the traits in Rust language. Rust uses one mechanism for both compile-time polymorphism and runtime polymorphism, which is elegant. However, the runtime polymorphism in Rust is not very flexible. Some traits cannot be used as a trait object, and it’s impossible to specify whether a method is resolved dynamically or statically. This shortage is reflected in my recursive function library RecurFn, and I develop a workaround, calling it customized trait object.

For example, assume we have a trait, Foo.

1
2
3
4
5
6
7
trait Foo {
fn foo(&self, f: impl Fn() -> ());
fn foo_twice(&self, f: impl Fn() -> () + Copy) {
self.foo(f);
self.foo(f)
}
}

In which method foo represents a polymorphic action, while method foo_twice represents a static action and should not be overridden.

This trait cannot be used as a trait object, because both methods have a impl Fn() -> () parameter. But we can turn it into a trait object in the following way: we can replace impl Fn() -> () by &dyn Fn() -> (), and we can statically resolve the method foo_twice.

So we can create a dynamic version of the trait Foo.

1
2
3
4
trait DynFoo {
fn dyn_foo(&self, f: &Fn() -> ());
// `foo_twice` does not present because we want it statically resolved.
}

The trait object dyn DynFoo is the customized trait object. We hope that dyn DynFoo can be treated as the trait object of Foo. We need to implement:

  1. For any pointer to the implementation of Foo, it can be converted to the pointer to dyn DynFoo.
  2. dyn DynFoo implements Foo.

For 1, it can be implemented by implements DynFoo for all implementations of Foo.

1
2
3
4
5
impl<F: Foo> DynFoo for F {
fn dyn_foo(&self, f: &dyn Fn() -> ()) {
self.foo(f)
}
}

For 2, it can be implemented by implements Foo for dyn Foo.

1
2
3
4
5
impl Foo for dyn DynFoo {
fn foo(&self, f: impl Fn() -> ()) {
self.dyn_foo(&f)
}
}

Now the following code compiles.

1
2
3
let foo: &dyn DynFoo = &FooImpl::new(); // FooImpl::new() returns an implementation of Foo.
foo.dyn_foo(&|| {});
foo.foo_twice(&|| {});

The problem seems to be solved. However, Rust has a weird rule: in impl Foo for dyn DynFoo, DynFoo has 'static lifetime, and DynFoo cannot has Send or Sync flag. The following code:

1
2
3
4
fn test_complex<'a>() {
let a: Box<DynFoo + Send + 'a> = Box::new(FooImpl::new());
a.foo_twice(&|| {});
}

cannot compile.

To solve this problem, we need to make four copies of the code, and change the impl part of each copy into impl<'a> Foo for DynFoo + 'a, impl<'a> Foo for DynFoo + 'a + Send, impl<'a> Foo for DynFoo + 'a + Sync, impl<'a> Foo for DynFoo + 'a + Send + Sync.

To reduce this duplication, we can create a macro to do it.

1
2
3
4
5
6
7
8
9
10
11
12
13
macro_rules! impl_dyn_with_markers {
($($marker:ident),*) => {
impl<'a> Foo for dyn DynFoo + 'a$( + $marker)* {
fn foo(&self, f: impl Fn() -> ()) {
self.dyn_foo(&f)
}
}
};
}
impl_dyn_with_markers! {}
impl_dyn_with_markers! {Send}
impl_dyn_with_markers! {Sync}
impl_dyn_with_markers! {Send, Sync}

You can find the example code here, this strategy is used in RecurFn.