Attempting to use enum variants as tuple values

Hello all,

I'm writing a program that controls street intersection traffic lights, as a learning exercise. One of the goals is to increase traffic safety by ensuring that potentially dangerous lights combinations are impossible (on the program level). For that purpose, I want to use enums.

As an example, let's say there is a street that goes in the north-south direction, and the intersection has two traffic lights - one at the upper part (top) that controls traffic from north to south, and one at the lower part (bottom) that controls traffic from south to north. Each traffic light has a main section (for the straight direction) and a left arrow.

For the green light, the forbidden combinations would be "Top Main + Bottom Arrow" and "Bottom Main + Top Arrow". All other combinations are allowed.

I'm using a state variable that determines "right of way" - it contains a collection of traffic light sections that should turn on the green light at a given moment. The variants of the enum type provide the allowed section combinations, and only them.

I expected to have something along the lines of the following code (it doesn't compile, of course):

enum Street {
	Meridian,
	Parallel,
}

enum LightType {
	MainTop,
	MainBottom,
	ArrowTop,
	ArrowBottom,
	None,
}

enum AllowedLightCombo {
	MainBoth(MainTop, MainBottom),
	ArrowBoth(ArrowTop, ArrowBottom),
	MainArrowTop(MainTop, ArrowTop),
	MainArrowBottom(MainBottom, ArrowBottom),
	MainOnlyTop(MainTop),
	MainOnlyBottom(MainBottom),
	ArrowOnlyTop(ArrowTop),
	ArrowOnlyBottom(ArrowBottom),
	None,
}

struct StreetLight {
	street: Street,
	light: AllowedLightCombo,
}

let r_o_w = StreetLight {
	street: Street::Meridian,
	light: AllowedLightCombo::MainBoth,
};

Eventually, I was able to get the modified code to run, but it looks quite ugly:

#![allow(unused)]

fn main() {

	#[derive(Debug)]
	enum Street {
		Meridian,
		Parallel,
	}

	#[derive(Debug)]
	enum MainTop {
		Dummy,
	}
	#[derive(Debug)]
	enum MainBottom {
		Dummy,
	}
	#[derive(Debug)]
	enum ArrowTop {
		Dummy,
	}
	#[derive(Debug)]
	enum ArrowBottom {
		Dummy,
	}

	#[derive(Debug)]
	enum LightType {
		MainBoth(MainTop, MainBottom),
		ArrowBoth(ArrowTop, ArrowBottom),
		MainArrowTop(MainTop, ArrowTop),
		MainArrowBottom(MainBottom, ArrowBottom),
		MainOnlyTop(MainTop),
		MainOnlyBottom(MainBottom),
		ArrowOnlyTop(ArrowTop),
		ArrowOnlyBottom(ArrowBottom),
		None,
	}

	#[derive(Debug)]
	struct StreetLight {
		street: Street,
		light: LightType,
	}


	let r_o_w_1 = StreetLight {
		street: Street::Meridian,
		light: LightType::MainBoth(MainTop::Dummy, MainBottom::Dummy),
	};
	let r_o_w_2 = StreetLight {
		street: Street::Meridian,
		light: LightType::ArrowBoth(ArrowTop::Dummy, ArrowBottom::Dummy),
	};

	println!("{:?}", r_o_w_1);
	println!("{:?}", r_o_w_2);
}
$ cargo run
   Compiling light v0.1.0 (rust/projects/light)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.12s                                               
     Running `rust/projects/light/target/debug/light`
StreetLight { street: Meridian, light: MainBoth(Dummy, Dummy) }
StreetLight { street: Meridian, light: ArrowBoth(Dummy, Dummy) }

Are there better ways to achieve that original goal ? What would be the good/recommended practice for cases like this ?

Thank you for your help.

I'd probably do something like this.

pub enum LightType {
    MainTop,
    MainBottom,
    ArrowTop,
    ArrowBottom,
}

#[derive(Clone, Copy)]
pub enum AllowedLightCombo {
    MainBoth,
    ArrowBoth,
    MainArrowTop,
    MainArrowBottom,
    MainOnlyTop,
    MainOnlyBottom,
    ArrowOnlyTop,
    ArrowOnlyBottom,
    AllRed,
}

impl AllowedLightCombo {
    pub fn lights(self) -> &'static [LightType] {
        use AllowedLightCombo::*;
        use LightType::*;
        match self {
            MainBoth => &[MainTop, MainBottom],
            ArrowBoth => &[ArrowTop, ArrowBottom],
            MainArrowTop => &[MainTop, ArrowTop],
            MainArrowBottom => &[MainBottom, ArrowBottom],
            MainOnlyTop => &[MainTop],
            MainOnlyBottom => &[MainBottom],
            ArrowOnlyTop => &[ArrowTop],
            ArrowOnlyBottom => &[ArrowBottom],
            AllRed => &[],
        }
    }
}

So it seems that there is no simple way to do it just by enum declarations, and additional procedural logic will be required. OK, that makes sense. Thank you !

I think you're essentially after enum variant types? Eg. so SomeEnum::Variant is also a type you can store in another enum.

Rust unfortunately doesn't support them, the traditional workaround is to have each variant store another type with the same name, which is sort of what you've done with the "dummy" enums (which, incidentally, are more naturally written as struct ArrowTop; etc ...)

There is this rather gnarly crate I found looking for a proposal for adding enum variants types: enum_variant_type - Rust

3 Likes

I think you're essentially after enum variant types? Eg. so SomeEnum::Variant is also a type
you can store in another enum.

Yes, exactly that.

Rust unfortunately doesn't support them, the traditional workaround is to have each variant
store another type with the same name, which is sort of what you've done with the "dummy"
enums

Yes, but it didn't look very nice...

(which, incidentally, are more naturally written as struct ArrowTop; etc ...)

Oh ! This definitely looks more natural and less cluttered. I just got stuck thinking about enums, while structs were just around the corner :slight_smile:

struct MainTop;
struct MainBottom;
 ......
let r_o_w_1 = StreetLight {
    street: Street::Meridian,
    light: LightType::MainBoth(MainTop, MainBottom),
};

It's a lot closer to what I wanted to begin with, even if not quite there. But it compiles, so I'll just use it, for simplicity.

There is this rather gnarly crate I found looking for a proposal for adding enum variants types:
enum_variant_type - Rust 1

OK, interesting, I'll keep that in mind for the future. For now, it's just a learning exercise for me, so I try to discover the language limits and to do as much as possible with the basic Rust features only.

Thank you, that was really helpful !

1 Like

For this, I’d probably use a virtual lockout-tagout system— Create a unique token for each conflict point between traffic flows, and only allow a light to turn green if it holds the token for every potential conflict along the path it controls. Then, don’t replace the tokens until a suitable safety time has passed after the light has turned red again.

2 Likes

Yes, but that would still rely on the program logic. It is more interesting to see how much of that logic could be off-loaded to the compiler instead.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.