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 Gearwith the variantsParkandDrive, astruct Carthat among other things contains the fieldgear: Gear, and let the different methods act in different ways depending on the variant ingear. - Create an
enum CarGearwith the variantsPark(Car)andDrive(Car)that wrap around somestruct Car, wherestruct Cardoesn't contain agearfield, since this is handled in the variants wrappingstruct Car: This way, every method will need to implement amatchstatement if it interacts with any data ofstruct Car, even ones that should be available for all variants and should act in identical ways. Thesematchs will also be the sole guards for limiting functionality of the different gears. - One
struct CarGearfor 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,
ParkandDrive), which implementsGearin an empty block. - One generic
struct Car<G: Gear>, with agear: PhantomDatafield 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: