Dynamize
In order to turn a trait into a trait object the trait must be object-safe and the values of all associated types must be specified. Sometimes you however want a trait object to be able to encompass trait implementations with different associated type values. This crate provides a procedural macro to achieve that.
The following code illustrates a scenario where dynamize can help you:
let client: HttpClient = ...;
let object = &client as &dyn Client;
The last line of the above code fails to compile with:
error[E0191]: the value of the associated type
Error
(from traitClient
) must be specified
To use dynamize you only have to make some small changes:
- You add the
#[dynamize::dynamize]
attribute to your trait. - You specify a trait bound for each associated type.
Dynamize defines a new trait for you, named after your trait but
with the Dyn
prefix, so e.g. Client
becomes DynClient
:
let client: HttpClient = ...;
let object = &client as &dyn DynClient;
The new "dynamized" trait can then be used without having to specify the associated type value.
How does this work?
For the above example dynamize generates the following code:
As you can see in the dynamized trait the associated type was replaced with the
destination type of the Into
bound. The magic however happens afterwards:
dynamize generates a blanket implementation: each type implementing Client
automatically also implements DynClient
!
How does this actually work?
The destination type of an associated type is determined by looking at its trait bounds:
-
if the first trait bound is
Into<T>
the destination type isT
-
otherwise the destination type is the boxed trait object of all trait bounds
e.g.Error + Send
becomesBox<dyn Error + Send>
(for this the first trait bound needs to be object-safe)
Dynamize can convert associated types in:
- return types, e.g.
fn example(&self) -> Self::A
- callback parameters, e.g.
fn example<F: Fn(Self::A)>(&self, f: F)
Dynamize also understands if you wrap associated types in the following types:
- tuples
Option<_>
Result<_, _>
some::module::Result<_>
(type alias with fixed error type)&mut dyn Iterator<Item = _>
Vec<_>
,VecDeque<_>
,LinkedList<_>
,HashSet<K>
,BinaryHeap<K>
,BTreeSet<K>
,HashMap<K, _>
,BTreeMap<K, _>
(forK
onlyInto
-bounded associated types work because they requireEq
)
Note that since these are resolved recursively you can actually nest these arbitrarily so e.g. the following also just works:
;
How does dynamize deal with method generics?
In order to be object-safe methods must not have generics, so dynamize simply moves them to the trait definition:
becomes
If two method type parameters have the same name, dynamize enforces that they also have the same bounds and only adds the parameter once to the trait.
Dynamize supports async
Dynamize supports async out of the box. Since Rust however does not yet support async functions in traits, you'll have to additionally use another library like async-trait, for example:
#[dyn_trait_attr(foo)]
attaches#[foo]
to the dynamized trait#[blanket_impl_attr(foo)]
attaches#[foo]
to the blanket implementation
Note that it is important that the #[dynamize]
attribute comes before the
#[async_trait]
attribute, since dynamize must run before async_trait.
Using dynamize with other collections
Dynamize automatically recognizes collections from the standard library like
Vec<_>
and HashMap<_, _>
. Dynamize can also work with other collection
types as long as they implement IntoIterator
and FromIterator
, for example
dynamize can be used with indexmap as
follows:
The passed number tells dynamize how many generic type parameters to expect.
- for 1 dynamize expects:
Type<A>: IntoIterator<Item=A> + FromIterator<A>
- for 2 dynamize expects:
Type<A,B>: IntoIterator<Item=(A,B)> + FromIterator<(A,B)>
- for 3 dynamize expects:
Type<A,B,C>: IntoIterator<Item=(A,B,C)> + FromIterator<(A,B,C)>
- etc ...