Combination of enum variants, with at most one per variant

Hi,

Suppose that you have to store a state that can assume different values (enum variants), but also a combination of that, but only one variant of each of them.
For example a game where each cell can be occupied by a player, by a scenario object or by a scroll:

enum CellState {
   Player(PlayerId),
   ScenarioObject(ObjectType),
   Scroll(ScrollType)
}

So in the same cell you can have both of all three, but only one player, one scenario object, one scroll at most.

Using a Vec<CellState> would not assure that the invariant is satisfied, so you are not sure that you have at most one per each variant.

I was thinking using a 3-tuple of Option<CellState>, but I don't like so much, and it seems not so nice to manage.

How you would design that?

Thank you.

Is CellState even needed? Aren't the semantics captured simply by the following:

struct Cell {
    playerId: Option<PlayerId>,
    scenarioObject: Option<ObjectType>,
    scroll: Option<ScrollType>,
    // etc
};

Now, this might not seem very extensible, but is the enum version any better in that regard, or does it just give an illusion of extensibility?

1 Like

In which way is the approach with a struct that contains three options not extensible?

If some fields are added in the future, it might be helpful to implement std::default::Default, which can, for example, be done as follows:

#[derive(Default)]
struct Cell {
    player_id: Option<PlayerId>,
    scenario_object: Option<ObjectType>,
    scroll: Option<ScrollType>,
    // etc
};
// the following line doesn't need to be changed if
// more fields are added to Cell in future
let cell = Cell { player_id: Some(p_id), ..Default::default() }

Thank you for your replies.

That could be a solution, I don't like so much that Cell size is increasing with number of variants (I know Option:None is not consuming too much memory), but anyway it is better than having a Vec<CellState>.

Thank you.

A Option always takes the same size in memory even if it is None.

But for some types like references or NonZeroU64 the Option will have the same size as the type
since it can use 0 as None

use std::mem;
use std::num::NonZeroU64;

#[derive(Copy, Clone)]
struct PlayerIdWithZero(u64);

#[derive(Copy, Clone)]
struct PlayerIdNonZero(NonZeroU64);

#[derive(Copy, Clone)]
enum ObjectType {
    Item,
    Player,
    Enemy,
}
fn main() {
    let player_id_with_zero = PlayerIdWithZero(42); // 8 byte
    let some_player_id_with_zero = Some(player_id_with_zero); // 16 byte
    let none_player_id_with_zero = Option::<PlayerIdWithZero>::None; // 16 byte

    let player_id_non_zero = PlayerIdNonZero(NonZeroU64::new(42).unwrap()); // 8 byte
    let some_player_id_non_zero = Some(player_id_non_zero); // 8 byte
    let none_player_id_non_zero = Option::<PlayerIdNonZero>::None; // 8 byte

    let object_type = ObjectType::Item; // 1 byte
    let some_object_type = Some(object_type); // 1 byte
    let none_object_type = Option::<ObjectType>::None; // 1 byte
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=8749de09d66af69960c5cdad265b1a04

Important distinction between “size” of a type and (total) memory consumption of a value: for types with indirections, None can take significantly less memory. A None-value of type Option<Box<[u8; 1000000]>> consumes a lot less memory than a Some-value.

Admitted the types in question PlayerId, ObjectType, ScrollType don’t necessarily sound like they’re containing much data or any indirections.