PrancingCrabulon
PrancingCrabulon

Reputation: 412

Mutually exclusive traits

I need to create operations for an operation sequence. The operations share the following behaviour. They can be evaluated, and at construction they can either be parametrized by a single i32 (eg. Sum) or not parametrized at all (eg. Id).

I create a trait Operation. The evaluate part is trivial.

trait Operation {
    fn evaluate(&self, operand: i32) -> i32;
}

But I don't know how to describe the second demand. The first option is to simply let concrete implementations of Operation handle that behaviour.

pub struct Id {}

impl Id {
    pub fn new() -> Id {
        Id {}
    }
}

impl Operation for Id {
    fn evaluate(&self, operand: i32) -> i32 {
        operand
    }
}

pub struct Sum {
    augend: i32,
}

impl Sum {
    pub fn new(augend: i32) -> Sum {
        Sum { augend }
    }
}

impl Operation for Sum {
    fn evaluate(&self, addend: i32) -> i32 {
        augend + addend
    }
}

Second option is a new function that takes an optional i32. Then the concrete implementations deal with the possibly redundant input. I find this worse than the first option.

trait Operation {
    fn evaluate(&self, operand: i32) -> i32;
    fn new(parameter: std::Option<i32>)
}

Google has lead me to mutually exclusive traits: https://github.com/rust-lang/rust/issues/51774. It seems promising, but it doesn't quite solve my problem.

Is there a way to achieve this behaviour?

trait Operation = Evaluate + (ParametrizedInit or UnparametrizedInit)

Upvotes: 3

Views: 2177

Answers (1)

Lukas Kalbertodt
Lukas Kalbertodt

Reputation: 88516

How about you use an associated type to define the initialization data?

trait Operation {
    type InitData;
    fn init(data: Self::InitData) -> Self;

    fn evaluate(&self, operand: i32) -> i32;
}

impl Operation for Id {
    type InitData = ();
    fn init(_: Self::InitData) -> Self {
        Id {}
    }

    fn evaluate(&self, operand: i32) -> i32 {
        operand
    }
}

impl Operation for Sum {
    type InitData = i32;
    fn init(augend: Self::InitData) -> Self {
        Sum { augend }
    }

    fn evaluate(&self, addend: i32) -> i32 {
        augend + addend
    }
}

For the Id case you specify () to say that the initialization does not need data. It's still a bit meh to call Operation::init(()), but I think the trait at least captures the logic fairly well.


To actually get mutually exclusive traits (which is apparently what you want), you have to use some workaround. The Rust language does not support mutually exclusive traits per-se. But you can use associated types and some marker types to get something similar. This is a bit strange, but works for now.

trait InitMarker {}

enum InitFromNothingMarker {}
enum InitFromI32Marker {}

impl InitMarker for InitFromNothingMarker {}
impl InitMarker for InitFromI32Marker {}

trait Operation {
    type InitData: InitMarker;

    fn init() -> Self
    where
        Self: Operation<InitData = InitFromNothingMarker>;
    fn init_from(v: i32) -> Self
    where
        Self: Operation<InitData = InitFromI32Marker>;
}

trait UnparametrizedInit: Operation<InitData = InitFromNothingMarker> {}
trait ParametrizedInit: Operation<InitData = InitFromI32Marker> {}

impl<T: Operation<InitData = InitFromNothingMarker>> UnparametrizedInit for T {}
impl<T: Operation<InitData = InitFromI32Marker>> ParametrizedInit for T {}

(Ideally you want to have a Sealed trait that is defined in a private submodule of your crate. That way, no one (except for you) can implement the trait. And then make Sealed a super trait for InitMarker.)

This is quite a bit of code, but at least you can make sure that implementors of Operation implement exactly one of ParametrizedInit and UnparametrizedInit.

In the future, you will likely be able to replace the marker types with an enum and the associated type with an associated const. But currently, "const generics" are not finished enough, so we have to take the ugly route by using marker types. I'm actually discussing these solutions in my master's thesis (section 4.2, just search for "mutually exclusive").

Upvotes: 5

Related Questions