Type-level programming in Rust
In Rust, there's a concept sometimes referred to as Type-level programming
. To employ this, you'll create a struct
, which will (among other things) contain a field of the zero-sized type PhantomData<T>
: this will tell the compiler that our struct
acts as if it stores a value of type T
, and will only be used at compile time for static analysis. Read more in the Rustonomicon.
I've written a small example where this concept is showcased by applying it to a car, which can be found here.
Depending on what gear it's in, we should be permitted to do different things, and some things should be available irrespective of what gear we're in. We'd also like to be able to change gear in a convenient manner. We'll stick with two gears, Park
and Neutral
. Some simple designs that don't employ type-level programming to handle this could be:
- Create an
enum Gear
with the variantsPark
andDrive
, astruct Car
that among other things contains the fieldgear: Gear
, and let the different methods act in different ways depending on the variant ingear
. - Create an
enum CarGear
with the variantsPark(Car)
andDrive(Car)
that wrap around somestruct Car
, wherestruct Car
doesn't contain agear
field, since this is handled in the variants wrappingstruct Car
: This way, every method will need to implement amatch
statement if it interacts with any data ofstruct Car
, even ones that should be available for all variants and should act in identical ways. Thesematch
s will also be the sole guards for limiting functionality of the different gears. - One
struct CarGear
for every gear, creating completely different types where the methods they should have in common will need to be individually implemented for every type.
There's another approach, that this example code presents:
- One
trait Gear
- One struct for each available concrete gear (that is,
Park
andDrive
), which implementsGear
in an empty block. - One generic
struct Car<G: Gear>
, with agear: PhantomData
field to keep track of what concrete type were using that implementsGear
. - Finally, and optionally if you'd like to have it all wrapped up in one type, an enum that wraps around
struct Car<G: Gear>
, whose variants map to the different types implementingGear
.
The optional step of wrapping the generic Car
inside an enum is done inside src/lib.rs
, with the rest of the code placed inside src/struct_car.rs
. A simplified overview of the code:
use std::marker::PhantomData;
trait Gear {}
struct Park;
struct Drive;
impl Gear for Park {}
impl Gear for Drive {}
struct Car<G: Gear> {
speed: i32,
locked_doors: bool,
gear: PhantomData<G>,
}
// These implementations will only be available for `Park`
impl Car<Park> {
/// ...
}
// These implementations will only be available for `Drive`
impl Car<Drive> {
/// ...
}
// These implementations will be available for all types implementing `Gear`
impl<G: Gear> Car<G> {
/// ...
}
/// Creating a common type
pub enum CommonEnumCar {
Park(Car<Park>),
Drive(Car<Drive>),
}
fn main() {
let mut car = CommonEnumCar::new();
car.into_drive();
if let Car::Drive(ref mut inner_car) = car {
inner_car.accelerate(3);
}
println!("Speed: {}", car.speed()); // Should display `3`
car.honk();
car.lock_doors();
car.into_park();
println!("Speed: {}", car.speed()); // Should display `0`
}
Other sources: