Your argument is very true, but in the larger picture it comes with an unpleasant aftertaste. A closer look at dyn
shows a mechanism for type erasure or so-called upcast. In essence it smashes the type system. We can go further and generate all necessary dispatch tables by hand, which allows for separation of dispatch table and object, and thus for general polymorphic ABIs.
For example, consider the simple generic power function
trait Monoid {
fn one() -> Self;
fn mul(&self, x: &Self) -> Self;
}
fn pow<T: Monoid>(x: T, n: u32) -> T {
let mut y = T::one();
for _ in 0..n {y = y.mul(&x);}
return y;
}
impl Monoid for i32 {
fn one() -> Self {1}
fn mul(&self, x: &Self) -> Self {self*x}
}
Now we would like pow
to be runtime polymorphic instead. At first the structure of the dispatch table:
struct MonoidPtr(Box<dyn std::any::Any>);
struct MonoidType {
one: &'static dyn Fn() -> MonoidPtr,
mul: &'static dyn Fn(&MonoidPtr,&MonoidPtr) -> MonoidPtr
}
Implement it for Monoid
:
fn monoid_type<T: Monoid + 'static>() -> MonoidType {
let one = &|| -> MonoidPtr {MonoidPtr(Box::new(T::one()))};
let mul = &|a: &MonoidPtr, b: &MonoidPtr| -> MonoidPtr {
if let Some(a) = a.0.downcast_ref::<T>() {
if let Some(b) = b.0.downcast_ref::<T>() {
return MonoidPtr(Box::new(a.mul(b)));
}
}
unreachable!();
};
return MonoidType{one,mul};
}
The actual polymorphic code (which is monomorphic from the compiler's point of view):
fn pow_poly(t: &MonoidType, x: &MonoidPtr, n: u32) -> MonoidPtr {
let mut y = (t.one)();
for _ in 0..n {y = (t.mul)(&y,&x);}
return y;
}
Finally a type safe wrapper:
fn pow<T: Monoid + 'static>(x: T, n: u32) -> T {
let t = monoid_type::<T>();
let px = MonoidPtr(Box::new(x));
return match pow_poly(&t,&px,n).0.downcast::<T>() {
Ok(y) => *y,
_ => unreachable!()
};
}