1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
|
# 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.
[trait object]: https://doc.rust-lang.org/book/ch17-02-trait-objects.html
[object-safe]: https://doc.rust-lang.org/reference/items/traits.html#object-safety
[associated types]: https://doc.rust-lang.org/book/ch19-03-advanced-traits.html
The following code illustrates a scenario where dynamize can help you:
```rust ignore
trait Client {
type Error;
fn get(&self, url: String) -> Result<Vec<u8>, Self::Error>;
}
impl Client for HttpClient { type Error = HttpError; ...}
impl Client for FtpClient { type Error = FtpError; ...}
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 trait `Client`)
> must be specified
To use dynamize you only have to make some small changes:
```rust ignore
#[dynamize::dynamize]
trait Client {
type Error: Into<SuperError>;
fn get(&self, url: String) -> Result<Vec<u8>, Self::Error>;
}
```
1. You add the `#[dynamize::dynamize]` attribute to your trait.
2. 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`:
```rust ignore
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:
```rust ignore
trait DynClient {
fn get(&self, url: String) -> Result<Vec<u8>, SuperError>;
}
impl<__to_be_dynamized: Client> DynClient for __to_be_dynamized {
fn get(&self, url: String) -> Result<Vec<u8>, SuperError> {
Client::get(self, url).map_err(|x| x.into())
}
}
```
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 is `T`
* otherwise the destination type is the boxed trait object of all trait bounds
e.g. `Error + Send` becomes `Box<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, _>`
(for `K` only `Into`-bounded associated types work because they require `Eq`)
Note that since these are resolved recursively you can actually nest these
arbitrarily so e.g. the following also just works:
```rust ignore
fn example(&self) -> Result<Vec<Self::Item>, Self::Error>;
```
## 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. For the following source code:
```rust
#[dynamize::dynamize]
trait Gen {
type Result: std::fmt::Display;
fn foo<A>(&self, a: A) -> Self::Result;
fn bar<A, B>(&self, a: A, b: B) -> Self::Result;
fn buz(&self) -> Self::Result;
}
```
dynamize generates the following trait:
```rust
trait DynGen<A, B> {
fn foo(&self, a: A) -> Box<dyn std::fmt::Display + '_>;
fn bar(&self, a: A, b: B) -> Box<dyn std::fmt::Display + '_>;
fn buz(&self) -> Box<dyn std::fmt::Display + '_>;
}
```
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.
Note that in the dynamized trait calling the `buz` method now requires you to
specify both generic types, even though they aren't actually required by the
method. You can avoid this by splitting the original trait in two, i.e. moving
the `buz` method to a separate trait, which can be dynamized separately.
## 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](https://crates.io/crates/async-trait), for example:
```rust ignore
#[dynamize::dynamize]
#[dyn_trait_attr(async_trait)]
#[blanket_impl_attr(async_trait)]
#[async_trait]
trait Client: Sync {
type Error: std::error::Error + Send;
async fn get(&self, url: String) -> Result<Vec<u8>, Self::Error>;
}
```
* `#[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.
## Dynamized supertraits
In Rust a macro only operates on the passed input; it does not have access to
the surrounding source code. This also means that a `#[dynamize]` macro cannot
know which other traits have been dynamized. When you want to dynamize a trait
with a dynamized supertrait, you have to tell dynamize about it with the
`#[dynamized(...)]` attribute:
```rust ignore
#[dynamize::dynamize]
trait Client {
type Error: std::error::Error;
fn get(&self, url: String) -> Result<Vec<u8>, Self::Error>;
}
#[dynamize::dynamize]
#[dynamized(Client)]
trait ClientWithCache: Client {
type Error: std::error::Error;
fn get_with_cache<C: Cache>(
&self,
url: String,
cache: C,
) -> Result<Vec<u8>, <Self as ClientWithCache>::Error>;
}
```
This results in `DynClientWithCache` having the dynamized `DynClient` supertrait.
With the above code both traits have independent associated types. So a trait
could implement one trait with one `Error` type and and the other trait with
another `Error` type. If you don't want that to be possible you can change the
second trait to:
```rust ignore
#[dynamize::dynamize]
#[dynamized(Client)]
#[convert = |x: <Self as Client>::Error| -> Box<dyn std::error::Error + '_> {Box::new(x) as _}]
trait ClientWithCache: Client {
fn get_with_cache<C: Cache>(
&self,
url: String,
cache: C,
) -> Result<Vec<u8>, <Self as Client>::Error>;
}
```
Note that we removed the associated type and are now using the associated type
from the supertrait by qualifying `Self as Client`. Since the `#[dynamize]`
attribute on the `ClientWithCache` trait however cannot know the associated
type from another trait, we also need to add a `#[convert = ...]` attribute to
tell dynamize how to convert `<Self as Client>::Error>`.
## 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](https://crates.io/crates/indexmap) as
follows:
```rust ignore
#[dynamize::dynamize]
#[collection(IndexMap, 2)]
trait Trait {
type A: Into<String>;
type B: Into<i32>;
fn example(&self) -> IndexMap<Self::A, Self::B>;
}
```
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 ...
|