Rust: Generics Considered Colorful
Published:
This post shows that Rust’s generics are colorful. I’ll demonstrate an example to show what I mean, and what the problems are.
Motivating Example
Consider this silly code:
trait MyTrait {
fn foo(&self);
}
struct S1;
impl MyTrait for S1 {
fn foo(&self) {
println!("S1::foo()");
}
}
fn call_foo<T>(t: &T) where T: MyTrait {
t.foo();
}
fn main() {
let s1 = S1{};
call_foo(&s1);
}
This seems fine so far.
Now, let’s suppose we have an collection of MyTraits
, like this:
// Previous code not shown.
struct S2;
impl MyTrait for S2 {
fn foo(&self) {
println!("S2::foo()");
}
}
fn main() {
let v: Vec<&dyn MyTrait> = vec![&S1{}, &S2{}];
for x in v {
call_foo(x);
}
}
This produces this compilation error:
Compiling playground v0.0.1 (/playground)
error[E0277]: the size for values of type `dyn MyTrait` cannot be known at compilation time
--> src/main.rs:28:18
|
28 | call_foo(x);
| -------- ^ doesn't have a size known at compile-time
| |
| required by a bound introduced by this call
|
= help: the trait `Sized` is not implemented for `dyn MyTrait`
The problem is that Rust generics are monomorphized, but monomorphization is not supported for trait objects.
call_foo
is a colored function. The code doesn’t compile because trait objects are the wrong color.
Does this Matter In Real Life?
Yes. Here’s an example: The Rust bindings for interacting with the Z3 theorem prover have a trait z3::ast::Ast
to represent terms, constants, and expressions. As you’re building a theory, you may want to maintain a vector of your constants in a Vec<Box<dyn z3::ast::Ast>>
. Once Z3 has constructed a model that satisfies your theory, you’ll probably want to query the model for the values of constants via the method pub fn get_const_interp<T: Ast<'ctx>>(&self, ast: &T) -> Option<T>
.
Well, you just shot your foot off. You can’t call this method on a trait object, so now you need to redo the work you just did. And the new code is going to be a whole lot uglier.
Fix 1: Prefer Trait Objects
In contrast to the orthodox Rust opinion, we should prefer to use trait objects unless we explicitly need to combine multiple trait bounds or dynamic dispatch is a performance issue. Here’s what I mean:
// Previous code not shown.
fn call_foo(x: &dyn MyTrait) {
x.foo();
}
fn main() {
let v: Vec<&dyn MyTrait> = vec![&S1{}, &S2{}];
for x in v {
call_foo(x);
}
}
Note that this trait object is general enough to work with many data structures. For example, we can still use a Box
with this implementation:
// Previous code not shown.
fn main() {
let v2: Vec<std::boxed::Box<dyn MyTrait>> = vec![std::boxed::Box::new(S1{}),
std::boxed::Box::new(S2{})];
for x in &v2 {
call_foo(x.as_ref());
}
call_foo(v2[0].as_ref());
}
And, of course, we can still use call_foo
on a specific instance:
// Previous code not shown.
fn main() {
let s = S1{};
call_foo(&s);
}
Fix 2: Always Implement Your Traits for Trait Objects
You should just always implement your traits for trait objects:
// Previous code not shown.
impl MyTrait for &dyn MyTrait {
fn foo(&self) {
(**self).foo();
}
}
fn call_foo<T>(x: &T) where T: MyTrait {
x.foo();
}
fn main() {
let v: Vec<&dyn MyTrait> = vec![&S1{}, &S2{}];
for x in v {
call_foo(&x);
}
}
Note that this code also works on other kinds of trait objects:
// Previous code not shown.
fn main() {
let v2: Vec<std::boxed::Box<dyn MyTrait>> = vec![std::boxed::Box::new(S1{}),
std::boxed::Box::new(S2{})];
for x in &v2 {
call_foo(&x.as_ref());
}
call_foo(&v2[0].as_ref());
let v3: Vec<std::rc::Rc<dyn MyTrait>> = vec![std::rc::Rc::new(S1{}),
std::rc::Rc::new(S2{})];
for x in &v3 {
call_foo(&x.as_ref());
}
call_foo(&v3[0].as_ref());
}
If you create a trait then you must be the one that implements it for trait objects. Per the coherence rule a trait can only be implemented for a type by the crate that defines the trait or defines the type.
Fix 3: Fix Rust
There’s a lot of code in the wild that share the same pain-point as the Z3 example I mentioned. It shouldn’t be difficult to use generics. Effective Rust does explain the reason for the current design rather well. But I feel like this is an area that can be improved on.