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 variants Park and Drive, a struct Car that among other things contains the field gear: Gear, and let the different methods act in different ways depending on the variant in gear.
  • Create an enum CarGear with the variants Park(Car) and Drive(Car) that wrap around some struct Car, where struct Car doesn't contain a gear field, since this is handled in the variants wrapping struct Car: This way, every method will need to implement a match statement if it interacts with any data of struct Car, even ones that should be available for all variants and should act in identical ways. These matchs 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 and Drive), which implements Gear in an empty block.
  • One generic struct Car<G: Gear>, with a gear: PhantomData field to keep track of what concrete type were using that implements Gear.
  • 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 implementing Gear.

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: