(I am not sure if this is more fit for the Internals forum).
I've tried to use the Rust type system to "make illegal states unrepresentable" as some functional programmers say. The problem is very simple, there's a discrete machine with a simple state (three booleans) with only few legal states and I can only cycle between them. It's a traffic light. After a while and many alternative solutuions I've created this one:
#![allow(dead_code)]
mod traffic_light {
use std::fmt;
#[derive(PartialEq, Eq)]
pub struct State { red: bool, green: bool, yellow: bool }
pub const RED: State = State { red: true, green: false, yellow: false };
pub const GREEN: State = State { red: false, green: true, yellow: false };
pub const YELLOW: State = State { red: false, green: false, yellow: true };
impl State {
pub fn next_state(&self) -> Self {
match *self {
RED => GREEN,
GREEN => YELLOW,
YELLOW => RED,
_ => panic!("Invalid traffic_light State"),
}
}
pub fn is_red_on(&self) -> bool { self.red }
pub fn is_green_on(&self) -> bool { self.green }
pub fn is_yellow_on(&self) -> bool { self.yellow }
}
impl fmt::Debug for State {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
RED => write!(f, "Red traffic light"),
GREEN => write!(f, "Green traffic light"),
YELLOW => write!(f, "Yellow traffic light"),
_ => panic!("Invalid traffic_light State"),
}
}
}
}
fn main() {
let mut light = traffic_light::RED;
println!("{:?}", light);
for _ in 0 .. 5 {
light = light.next_state();
println!("{:?}", light);
}
}
But that code isn't DRY and those panic aren't nice.
To remove the panics I've bundled the state inside variants of an enum:
#[derive(Debug)]
#[repr(u32)]
enum TrafficLight {
Red = 0b_1_0_0,
Green = 0b_0_1_0,
Yellow = 0b_0_0_1,
}
This is efficient but not general enough...
So I think I'd like to write something like this:
#[derive(Debug)]
enum TrafficLight {
Red { red: bool = true, green: bool = false, yellow: bool = false },
Green { red: bool = false, green: bool = true, yellow: bool = false },
Yellow { red: bool = false, green: bool = false, yellow: bool = true },
}
impl TrafficLight {
fn next_state(&self) -> TrafficLight {
use TrafficLight::*;
match *self {
Red => Green,
Green => Yellow,
Yellow => Red,
}
}
}
fn main() {
let mut light = traffic_light::Red;
println!("{:?} {}", light, light.red);
for _ in 0 .. 5 {
light = light.next_state();
println!("{:?}", light);
}
}
This requires no impl Debug, it contains no panics, and it doesn't need functions like is_red_on().
There was a proposal for "Default values for struct fields" that could allow this, but it's frozen or dead:
https://github.com/rust-lang/rfcs/issues/1594
I also think two standard libray functions (next_variant()
and next_variant_wrapping()
) should be avilable to advance to the next variant of an enum (as long as enum variants are simple or with fully filled data):
fn next_state(&self) -> TrafficLight {
next_variant_wrapping(self)
}
Edit: you can do it in TypeScript, apparently (from Leverage union types in Typescript to avoid invalid states ):
type TrafficLightState =
{ red: true; yellow: false; green: false; } |
{ red: false; yellow: true; green: false; } |
{ red: false; yellow: false; green: true; }
function nextTrafficLightState(
trafficLight: TrafficLightState): TrafficLightState {
if (trafficLight.red) {
return { red: false, yellow: true, green: false };
} else if (trafficLight.yellow) {
return { red: false, yellow: false, green: true };
} else if (trafficLight.green) {
return { red: true, yellow: false, green: false };
}
}
Here TrafficLightState is only allowed to have those three values.