Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Avoiding PhantomData in a struct to enforce type constraints

Tags:

rust

I'm trying to develop a kind of batch system. Within that I'd like to use some kind of Process struct, which owns all process related parts. The current implementation uses PhantomData to enforce the type constraints:

pub struct Process<P: Producer<U>, T: Transformer<U, V>, C: Consumer<V>, U,V> 
{
    producer: P,
    transformer: T, 
    consumer: C,
    p1: PhantomData<U>,
    p2: PhantomData<V>,
}

The idea is that type emitted by the Producer will be used by the Transformer (maybe to a different type) and consumed by the Consumer. Therefore the types must match.

The Process struct should own the items implementing the Producer, Transformer and Consumer traits. I think that's why I need to use type parameters. Since I cannot use the the trait directly like

...
producer: Producer<U>,
...

because of the unknown size at compile time.

Is there a better way of doing this? I'm pretty new to Rust, so I might be thinking in the wrong direction.

The solution works, but it looks a bit odd with those PhantomData fields. Maybe that is just what PhantomData is used for?

like image 694
Nikolai Hellwig Avatar asked Feb 08 '23 16:02

Nikolai Hellwig


1 Answers

Instead of type parameters, you want associated types:

trait Producer {
    type Output;
    fn produce(&self) -> Self::Output;
}

trait Transformer {
    type Input;
    type Output;
    fn transform(&self, val: Self::Input) -> Self::Output;
}

trait Consumer {
    type Input;
    fn consume(&self, val: Self::Input);
}

struct Process<P, T, C>
    where P: Producer,
          T: Transformer<Input = P::Output>,
          C: Consumer<Input = T::Output>
{
    producer: P,
    transformer: T,
    consumer: C,
}

impl<P, T, C> Process<P, T, C>
    where P: Producer,
          T: Transformer<Input = P::Output>,
          C: Consumer<Input = T::Output>
{
    fn run(&self) {
        let a = self.producer.produce();
        let b = self.transformer.transform(a);
        self.consumer.consume(b);
    }
}

struct MakeNum;
impl Producer for MakeNum {
    type Output = u8;
    fn produce(&self) -> u8 { 41 }
}

struct AddOne;
impl Transformer for AddOne {
    type Input = u8;
    type Output = u8;
    fn transform(&self, val: u8) -> u8 { val + 1 }
}

struct PrintNum;
impl Consumer for PrintNum {
    type Input = u8;
    fn consume(&self, val: u8) { println!("Value was {}", val) }
}

fn main() {
    let process = Process {
        producer: MakeNum,
        transformer: AddOne,
        consumer: PrintNum,
    };

    process.run();
}

Although I wouldn't normally add the where clause on the struct proper, I'd just have it on the impl which would also have a new method that ensures the constraints just as well.

like image 114
Shepmaster Avatar answered Mar 28 '23 06:03

Shepmaster