I was experimenting with Rust's generic trait system to see what's possible and wrote the code below which is similar to one of the snippets on this post by @nikomatsakis.
First define a type Matryoshka (as in the dolls) which just takes a generic type and owns one instance of it.
struct Matryoshka<T> {
x: T,
}
Then define a trait Onion, which just means the objects are layered: you can peel a layer or pile on a layer.
To my surprise, this code compiles and works fine. The examples here really demonstrate what is happening: Playground.
However, I would like to learn more about what are the hidden costs that I'm not aware of. How would you recommend benchmarking this code? My initial goal was to implement this using C++ templates (so far I've failed), and I'm really curious to know what are the pros and cons of Rust when it comes to such a construction.
Is there a simpler or more efficient way of implementing this? One thing I'm not super happy about is that not only the implementation of Onion, but even its definition involves the type Matryoshka, which if possible I'd like to abstract away. Is there away to do so?
impl<T> Onion<T> for Matryoshka<T>, this means that for any type which is contained in a Matryoshka you can call the following functions (pile and peel) on the Matryoshka.
Therefore, pile will allow us to wrap the contents in another Matryoshka because it falls under the implementation generic over T, but only if we can somehow produce a Matryoshka, which in this case is self.
Peel on the other hand is okay with anything being returned, therefore it doesn't have to produce a Matryoshka, so it can just unwrap our Matryoshka
As to why you get the same size for every Matryoshka we pile: What else would there be to store at runtime? Rust has no built in reflection so there's no type data, and the compiler is free to optimize away.
As to making it a nicer model, while keeping the Onion<T> trait mostly intact: Ack! This is mostly identical to @leudz's implementation, I just added a few checks to make sure you can't return an illogical value.
use std::ops::Deref;
struct Matryoshka<T>(T);
//You could use a marker trait if you would like to prevent actual Derefing
impl<T> Deref for Matryoshka<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
trait Onion<T>: Deref<Target = T> + Sized {
type Wrapper: Deref<Target = Self>;
fn pile(self) -> Self::Wrapper;
fn peel(self) -> T;
}
impl<T> Onion<T> for Matryoshka<T> {
type Wrapper = Matryoshka<Matryoshka<T>>;
fn pile(self) -> Self::Wrapper {
Matryoshka(self)
}
fn peel(self) -> T {
self.0
}
}
There are not too many as far as I'm aware of, they're just functions that are called by moving a Self into them. The only cost you could incur is a compilation time cost if you repeat it say, 1000 times?
I'm not 100% familiar with templates, all that I know is that they're kind of fake generics, in that the typename passed is essentially copy/pasted everywhere it's referenced and hope for no errors...
This doesn't appear to be related to the Curiously Recurring Template Pattern, though. CRTP has to do with inheriting from a base class parameterized with Self. Rust doesn't have classes or inheritance, so the idiom really doesn't translate. My understanding is that it's mostly used for two things: 1) to achieve compile time polymorphism in ways that Rust supports natively through traits, and 2) to do clever things with inheritance, which Rust doesn't have at all.
You just have a struct that contains a generic T and can be wrapped and unwrapped. Unless I am missing something, this would work fine in C++ without CRTP, or without inheritance at all, really.
But in rust, we don't use base classes for dynamic polymorphism. We use traits. When you implement a trait, you already know the Self type: (which is effectively the "zeroth" type parameter to every trait)
trait Trait {
fn foo(&mut self);
fn call_foo_twice(&mut self) {
self.foo(); // <-- this is statically dispatched
self.foo();
}
fn wrap(self) -> Wrapper<Self> { // <-- we can name Self
Wrapper(self)
}
}