I want to define two enums, almost identical except for one method (partial_cmp
).
In a object-oriented language, i would define the second one as a child class of the first one, and simply change this method.
I'm wondering what's the best way to do it in rust.
One idea (below) would be to define a trait with all the common methods with default implementation.
Then, create the two enums and to implement the partial_cmp
for each of them.
I see two problems with this way:
A, B, C
so it's still readable but what if i have 20 ?)From<char>
method below). For such methods, i need to write the same code twice.trait MyTrait {
/* common methods with default implementations */
}
// First Enum
#[derive(PartialEq, PartialOrd, Eq, Ord)]
enum MyEnum1 { A, B, C }
impl From<char> for MyEnum1 {
fn from(value: char) -> Self {
match value {
'A' => Self::A,
'B' => Self::B,
'C' => Self::C,
_ => panic!("unexpected char: {value}")
}
}
}
impl MyTrait for MyEnum1 {}
impl PartialOrd for MyEnum2 {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
/* partial_cmp for MyEnum1 */
}
}
// Second Enum
#[derive(PartialEq, PartialOrd, Eq, Ord)]
enum MyEnum2 { A, B, C }
impl From<char> for MyEnum2 {
fn from(value: char) -> Self {
match value {
'A' => Self::A,
'B' => Self::B,
'C' => Self::C,
_ => panic!("unexpected char: {value}")
}
}
}
impl MyTrait for MyEnum2 {}
impl PartialOrd for MyEnum2 {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
/* partial_cmp for MyEnum2 */
}
}
Composition is the easiest way to implement something like subclassing in Rust. That allows you to avoid duplicating the cases, but it does have the downside that you can't directly construct MyEnum2
the same way you would with an enum
. You can also avoid duplicating a lot of the code in your other impls by delegating the implementation to MyEnum1
, but you do still have to write the impls, unlike with a subclass.
The standard library actually has some wrapper types like Reverse
that exist purely to modify standard trait impls of the wrapped type. They work exactly this way (although you usually don't keep those types around for anything except the comparison.
use std::cmp::Ordering;
trait MyTrait {
/* common methods with default implementations */
}
// First Enum
#[derive(PartialEq, Eq)]
enum MyEnum1 {
A,
B,
C,
}
impl From<char> for MyEnum1 {
fn from(value: char) -> Self {
match value {
'A' => Self::A,
'B' => Self::B,
'C' => Self::C,
_ => panic!("unexpected char: {value}"),
}
}
}
impl MyTrait for MyEnum1 {}
impl PartialOrd for MyEnum1 {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
todo!("partial_cmp for MyEnum1")
}
}
// Second Enum
#[derive(PartialEq, Eq)]
struct MyEnum2(MyEnum1);
impl From<char> for MyEnum2 {
fn from(value: char) -> Self {
// delegate to `MyEnum1`'s impl, wrapping it in `MyEnum2`
Self(From::from(value))
}
}
impl MyTrait for MyEnum2 {}
impl PartialOrd for MyEnum2 {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
todo!("partial_cmp for MyEnum2")
}
}
If you really want to be able to construct MyEnum2
the same way you would MyEnum1
, you can add const
s for the enum cases (iirc this is how enum cases without fields are implemented under the hood)
impl MyEnum2 {
const A: MyEnum2 = MyEnum2(MyEnum1::A);
}