How to required different Trait bound depending on an enum variant

Hi,

I am trying to build some kind of framework API.

Let's say I have I have two traits, TraitA and TraitB.
I have an enum FormatEnum with two variant A and B.
I have struct MyStruct that has various field and one of them is "format: FormatEnum".

I want to have a generic method on MyStruct, let's call it "fn process(data:T) -> Result<()>" that required a trait bound on T but that is different acording to the "format" field.

Is there a way to do that ?

Here below some kind of pseudo code to illustrate.

trait TraitA {}

trait TraitB {}

enum FormatEnum {
    A,
    B,
}

struct MyStruct {
    field1: bool,
    ...,
    format: FormatEnum,
}

impl MyStruct {
    fn new(format: FormatEnum) -> MyStruct {
        MyStruct { field1: true, ..., format }
    }
    
    fn process<T:TraitA if self.format is FormatEnum::A>(&self, data:T) -> Result<()>{
        ...
        Ok(())
    }

    fn process<T:TraitB if self.format is FormatEnum::B>(&self, data:T) -> Result<()>{
        ...
        Ok(())
    }
}

On the user side, I would like for them to chose the format they want and then the code to required the correct trait.

You would need the value of the format to be a compiler-time concept associated to your type. E.g. with a const generic, but using custom types there is still unstable

#![feature(adt_const_params)]

trait TraitA {}

trait TraitB {}

type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;

#[derive(PartialEq, Eq)]
enum FormatEnum {
    A,
    B,
}

struct MyStruct<const FORMAT: FormatEnum> {
    field1: bool,
}

impl<const FORMAT: FormatEnum> MyStruct<FORMAT> {
    fn new() -> Self {
        MyStruct { field1: true }
    }
}
impl MyStruct<{FormatEnum::A}> {
    fn process<T: TraitA>(&self, data: T) -> Result<()> {
        Ok(())
    }
}
impl MyStruct<{FormatEnum::B}> {
    fn process<T: TraitB>(&self, data: T) -> Result<()> {
        Ok(())
    }
}

or you could use a const generic bool on stable; or you can use a type that’s sort-of “encoding” an enum-like value, possibly with a sealed trait, too. Feel free to ask for a code example if it isn’t clear what I mean by these alternatives.

2 Likes

Thank you so much for an answer !
Indeed it is not clear to me what do you mean by these alternatives. A code example will be greatly appreciated.

Using a bool

trait TraitA {}

trait TraitB {}

type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;

struct MyStruct<const IS_FORMAT_A: bool> {
    field1: bool,
}

impl<const IS_FORMAT_A: bool> MyStruct<IS_FORMAT_A> {
    fn new() -> Self {
        MyStruct { field1: true }
    }
}
impl MyStruct<true> {
    fn process<T: TraitA>(&self, data: T) -> Result<()> {
        Ok(())
    }
}
impl MyStruct<false> {
    fn process<T: TraitB>(&self, data: T) -> Result<()> {
        Ok(())
    }
}

Using types and a trait to encode type-level information comparable to an enum:

use std::marker::PhantomData;

pub trait TraitA {}

pub trait TraitB {}

type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;

pub trait Format: private::Sealed {}

// the sealed traits pattern (feel free to search for this if you're unfamiliar)
mod private {
    // ^^^^ this module is private (i.e. no `pub`)!
    pub trait Sealed {} // public trait in private module
}

pub enum FormatA {} // uninhabited type for type-level information
pub enum FormatB {}

// make the two `Format*` types a `Format` and nothing else.
impl private::Sealed for FormatA {}
impl Format for FormatA {}
impl private::Sealed for FormatB {}
impl Format for FormatB {}

pub struct MyStruct<Fmt: Format> {
    field1: bool,
    marker: std::marker::PhantomData<fn() -> Fmt>,
}

impl<Fmt: Format> MyStruct<Fmt> {
    fn new() -> Self {
        MyStruct {
            field1: true,
            marker: PhantomData,
        }
    }
}
impl MyStruct<FormatA> {
    fn process<T: TraitA>(&self, data: T) -> Result<()> {
        Ok(())
    }
}
impl MyStruct<FormatB> {
    fn process<T: TraitB>(&self, data: T) -> Result<()> {
        Ok(())
    }
}
1 Like

Also: In the “type-level information” approach, in case you want to write some generic methods over Fmt: Format that merely need the format information at runtime, you could add a method to the Format trait to allow reifying the information as a run-time value.

Also note that, since the two .process methods are simply two unrelated methods with the same name; with none of these approaches will you be able to call .process in a generic context with an unknown generic Fmt: Format or const FORMAT: … format, even if you have a type T that implements the union of all the required traits.

Thank you. I understand the example with the bool, it's basically the same than the first one just a litle bit different. the last example with the PhantomData marker.... I am lost to be honest (I know the sealed traits pattern). So you change the enum Format to a trait Format, you impl on two empty enum FormatA and FormatB (that I guess could be unit-like struct at this point)... And then the marker: PhantomData confuses me.

How does it looks when using MyStruct ? I mean how will the user instantiate it and call process on data ?

It works quite similar to the const generics version. If there’s something you feel behaves different, feel free to give a code example, and I can address it.


The marker is just there because the rust compiler requires all generic type arguments to be used in some field’s type. The fn() -> … inside of the PhantomData is used in order to indicate that this is just a marker type and we don’t want to pretend we’re actually containing a value of type Fmt; in this case it’s probably entirely unnecessary to add the fn() -> part, but I did it out of habit.


Yes, they could be anything, but since they’re intended to be used exclusively for type-level information, it makes sense to make them as unusable as possible…

though using unit structs can have advantages, too. Imagine adding a format: Fmt argument to the constructor, then you could call it as MyStruct::new(FormatA) instead of using turbo-fish MyStruct::<FormatA>::new(). I’m not sure what the “best” approach is, choose any you like the most.

1 Like

MyStruct::new(FormatA) is more ideal with what I am trying to do.

My problem is more complicated in fact. Here I gave the basic example with only two variant, but in fact I have somethink like 8 variants, 6 of them required the data to implement TraitA, and the last 2 variants required the data to implement TraitB.

What would you advise to do ?

My naive simple solution was just to required T: TraitA + TraitB but is it doable to do without that ?

If requiring T: TraitA + TraitB doesn’t create significant problems for your users, this might well be the easiest solution. Also remember that (unlike in languages without a static type system, or languages with lots of highly unsafe operations) refactoring in Rust is quite easy and fun, so you could always change your mind later.

1 Like

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.