Currently, when you see a generic type in Rust, implementation considerations are left entirely up to the compiler so long as the correct behavior is implemented for each type.
Rustc attempts to determine when fewer copies of functions are necessary and avoid making those copies - known as "polymorphization". As a result of polymorphization, items collected during monomorphization cannot be assumed to be monomorphic.
While it's great that this idea of polymorphization can be extended to more advanced cases, I think it might (emphasis on might) be worthwhile to surface some syntax for explicitly polymorphic code. Code that looks and is typed generically, but which is required to have only a single implementation.
Doing this will open up the full power of type-level generics for Trait Objects so long as some constraints are met. Certainly there are many cases where recognizing/enforcing polymorphism would be difficult (or undesirable), but I think there's a fairly expressive set of cases where it can be done and might be worth the effort.
The simplest example might be:
// polymorphic
fn pointer_identity<T>(v: &T) -> &T {
v
}
Here we have a function that is generic, but there's really only one possible implementation. We don't need to know the size, alignment, or offsets for the generic type T. This is clearly a bit simplistic, but any situation in which you're managing pointers to types without de-referencing the pointer is a candidate.
If you're writing code with trait objects, this could happen quite a bit (though there's currently no object safe generic methods). Composing dyn closures should be doable polymorphically. If this were expressible in Rust, you could write fully generic code without any monomorphization.
fn pointer_compose<T, R, S>(
a: &dyn Fn(R) -> S,
b: &dyn Fn(T) -> R,
) -> Box<dyn Fn(T) -> S> {
Box::new(|v| a(b(v)))
}
The size of T, R, and S will all matter at some point, but I think they don't matter here. Everything captured by this closure has a known size and the closure is boxed so it's known too.
Why does that matter? Well, it opens up trait objects to generics which means code is properly co-located and accessible on type-erased structs. There are situations where code size matters, like compiling to wasm.
It might also make compilation easier in some cases if the compiler doesn't need to consider the call sites for some generic code. If your API's generics are all polymorphic, users of your library don't need to compile new monomorphic versions of generic functions.
What are the downsides?
This might fragment the rust ecosystem a bit. Writing libraries which have a given size regardless of use (no monomorphization) seems laudable but may come at the cost of performance even in situations where monomorphization wouldn't have contributed to code size.
Pretty much everything generic could be done at least two ways. One with fat pointers and pointers and one with generics compiled into concrete types. Would we see an explicitly polymorphic implementation of std::Vec
that only stores pointers?
It's yet another thing to learn and yet another choice to make as you design your software.
Update: What about struct definitions?
Another thing to consider under the same umbrella is that for this to expand how useful it might be, it would be beneficial to be able to have generic structs with a single implementation.
For example, consider the follow struct that carries two references. (I've dropped lifetime annotations here to avoid the extra clutter)
struct KnownSizeGenericStruct<T,R> {
p1: &T,
p2: &R,
count: u8
}
The way this struct is laid out in memory doesn't depend on T or R directly. A polymorphic function should be able to create a KnownSizeGenericStruct
for any chosen T or R. Code that later dereferences p1
or p2
would need to know, but that's not a problem.
impl <T,R> KnownSizeGenericStruct<T,R> {
fn print_sizes(&self) {
println!(
"p1: {}, p2: {}",
size_of_val(self.p1),
size_of_val(self.p2)
);
}
}