Best way to define a type which is enumerable in multiple ways?

For example, let's define a type A, which is enumerable by B: B1 or B2, and also enumerable by C: C1 or C2.
So we can organize it by enumerating first by B then by C:

enum A {
    B1 {
        b1_common: bool,
        b1_c: B1C,
    },
    B2 {
        b2_common: bool,
        b2_c: B2C,
    }
}

enum B1C {
    C1 {
        c1_common: bool,
        b1_c1: bool,
    },
    C2 {
        c2_common: bool,
        b1_c2: bool,
    }
}

enum B2C {
    C1 {
        c1_common: bool,
        b2_c1: bool,
    },
    C2 {
        c2_common: bool,
        b2_c2: bool,
    }
}

Or I can reorganize it by enumerating first by C then by B:

enum A {
    C1 {
        c1_common: bool,
        c1_b: C1B,
    },
    C2 {
        c2_common: bool,
        c2_b: C2B,
    }
}

enum C1B {
    B1 {
        b1_common: bool,
        b1_c1: bool,
    },
    B2 {
        b2_common: bool,
        b2_c1: bool,
    }
}

enum C2B {
    B1 {
        b1_common: bool,
        b1_c2: bool,
    },
    B2 {
        b2_common: bool,
        b2_c2: bool,
    }
}

But either way, I has to duplicate some common field and some common method.
I have also considered using generic type:

enum A {
    B1 {
        b1_common: bool,
        b1_c: C<C1B1, C2B1>,
    },
    B2 {
        b2_common: bool,
        b2_c: C<C1B2, C2B2>,
    }
}

enum C<C1B, C2B> {
    C1 {
        c1_common: bool,
        c1_b: C1B,
    },
    C2 {
        c2_common: bool,
        c2_b: C2B,
    }
}

struct C1B1(bool);
struct C1B2(bool);
struct C2B1(bool);
struct C2B2(bool);

In this way, I avoid duplicating common fields. But I have some problems when implementing methods for C. Is it a good idea?
Are there any better ways?

Is there a reason you can't do it via composition?

struct A(B, C);

enum B {
    One(bool),
    Two(bool),
}

enum C {
    One(bool),
    Two(bool),
}

There are some dependencies, i.e. the b1_c1, b1_c2, b2_c1, b2_c2 things.

Can you be more specific about this part? Generics are a reasonable way to factor out common fields, but it's hard to say how well it would work here without knowing what kinds of problems you're having

1 Like

What order of B and C variants are we taking about here? If it's really only two each, I'd just have one enum manually expanding out each: enum A { B1C1, B1C2, B2C1, B2C2 }

Otherwise, it shouldn't really matter if you need to match on both B and C variations anyway. You can add some helpers that give you an enum of references to common, eg:

impl A {
  fn b_common(&self) -> BCommon;
}

enum BCommon<'a> {
  B1(&'a B1Common),
  B2(&'a B2Common),
}

but you might need a bunch of variances based on if it's mutable, etc. (Generics help a little here)

enum A { B1C1, B1C2, B2C1, B2C2 } will have more duplicate fields.

enum A {
    B1C1 {
        b1_common: bool,
        c1_common: bool,
        b1_c1: bool,
    },
    B1C2 {
        b1_common: bool,
        c2_common: bool,
        b1_c2: bool,
    },
    B2C1 {
        b2_common: bool,
        c1_common: bool,
        b2_c1: bool,
    },
    B2C2 {
        b2_common: bool,
        c2_common: bool,
        b2_c2: bool,
    }
}

But overall is likely to be simpler in practice. Since it's an enum, it's not like you're wasting more memory. If you have lots of common fields, you can pull those out into an explicit type.

An enum has a single "dimension", but you want to enumerate the data in two dimensions (B and C), so I don't think it is possible to do what you want in a single enum. Instead, consider using two enums, one for each dimension of enumeration. For example,

struct A {
    b: B,
    c: C,
    b_and_c: bool,
}

enum B {
    B1 { b1_common: bool },
    B2 { b2_common: bool },
}

enum C {
    C1 { c1_common: bool },
    C2 { c2_common: bool },
}

fn main() {
    let a = A {
        b: B::B1 { b1_common: true },
        c: C::C1 { c1_common: true },
        b_and_c: true,
    };
    // Match the enumeration for B.
    match a.b {
        B::B1 { b1_common } => {
            println!("Got B1");
        }
        B::B2 { b2_common } => {
            println!("Got B2");
        }
    }
1 Like

But in most cases, there will not be a common b_and_c: bool field, there may be b1_c1: bool, b1_c2: String, b2_c1: Option<i32> and b2_c2: Option<Box<A>>.
Moreover, a b_and_c: BC where BC is four variants enum is also problematic, because

  1. b or c and b_and_c may not be consistent with each other, i.e. b is b1 but b_and_c is b2_c1.
  2. it takes one more matching step to read b_and_c field.

Rust doesn't have a multidimensional enum, and there is no simple way to emulate it that also maintains the type safety. As @semicoleon said, turning either B or C into a generic type is one of the least bad ways. We might be able to help you more if you could tell us the problems you had when implementing methods for C.

1 Like

@semicoleon @doublequartz I found it's hard to implement methods for the generic type C.

  1. I need to define a helper trait T which is called in impl<C1B, C2B> C<C1B, C2B> where C<C1B, C2B>: T to define a common routine, and implement T for C<C1B1, C2B1>, C<C1B2, C2B2>. Is this ok?
  2. I'm worried about duplication code generated by using generic types. There are some methods, which only access non-generic fields. If I define them in impl<C1B, C2B> C<C1B, C2B>, I think the method will be duplicated per generic type. So I need to define them outside the impl block, and pass the fields them access as parameters to them. Is this ok?

Since you want a product type, I'd use a struct, not an enum:

enum B {
    B1(bool),
    B2(bool),
}

enum C {
    C1(bool),
    C2(bool),
}

struct A {
    b: B,
    c: C,
    bc: bool,
}

It's not a product type, it's a dependent product type. See Best way to define a type which is enumerable in multiple ways? - #9 by LambdaAlpha

  1. If C1B1 and C1B2 is the same, it does not need to be generic. if they are different but shares some interface, then yes, traits are the right tool for it. You could implement helper traits for C or for CnBn, whichever makes more sense.
  2. Any duplicate code will probably be optimized away. If it doesn't because of e.g. layout difference, you could try boxing them.
1 Like