Better solution than PhantomData

I would like to be able to write code that uses a generic factory type to create instances of some other generic type. The solution should allow fully static dispatch. This is what I use so far:

#[derive(Debug)]
enum Kind {
    One,
    Two,
}

trait Foo {
    fn get_kind(&self) -> Kind;
}

trait Factory {
    type Item: Foo;
    fn new_item () -> Self::Item;
}

// first version of Factory and Foo implementation
struct Foo1;
impl Foo for Foo1 {
    fn get_kind(&self) -> Kind {
        Kind::One
    }
}
struct Factory1;
impl Factory for Factory1 {
    type Item = Foo1;
    fn new_item () -> Self::Item {
      Foo1 { }
    }
} 

// second version of Factory and Foo implementation
struct Foo2;
impl Foo for Foo2 {
    fn get_kind(&self) -> Kind {
        Kind::Two
    }
}
struct Factory2;
impl Factory for Factory2 {
    type Item = Foo2;
    fn new_item () -> Self::Item {
      Foo2 { }
    }
} 

 
struct Bar<T: Factory> {
    phantom: core::marker::PhantomData<T> 
}


impl<T: Factory> Bar<T>  {
    fn priv_do() -> T::Item {
        T::new_item()
    }
    fn exe() -> T::Item {
        Self::priv_do()
    }
}

fn main() {
    type Bar1 = Bar::<Factory1>;
    type Bar2 = Bar::<Factory2>;
    println!("{:?}", Bar1::exe().get_kind());
    println!("{:?}", Bar2::exe().get_kind());
}

playground

Is there a more elegant way to implement this? Especially the PhantomData part feels like a hack.

Is there a reason the Factory types need to be separate from the created types? Could you implement the trait directly on the created types?

It would also help to know the overrall thing you're trying to achieve here, why do you want a factory?

2 Likes

Are Factory1 and Factory2 actually unit structs? If so, why do they need to exist at all rather than just parameterizing Bar with Foo1 and Foo2 directly? What further functionality does Bar<T> add that you couldn't have with just a T and free functions exe and priv_do?

I'm toying with nom. It is a parser combinator written in Rust. I want to be able to write a parser that creates a different output depending on the factory type the caller did use.

I imagine implementing my "grammar" functions once (... this would be the Bar type in my example above) and then use a differing factory impl for test and production scenarios. Due to static dispatch I would not lose any performance :slight_smile: .. hopefully.

But to answer the questions: The factories and the objects they create can have a completely different layout. The parser implementation however should not worry about the creation of the Foo-Objects and how they are represented or implemented internally.

No, Bar is actually a unit struct. I use it to somehow manage the static dispatch between the Factory implementations. Only the concrete factory implementation should know how to construct a concrete Foo1, Foo2 or FooX.

But you are right Bar<T> does not provide any further functionality. The benefit of doing this is that I have an impl block that contains a set of functions that share the same factory type. This way I do not need to repeat the generic Trait bounds for each function over and over again. Also the caller can do the switch once with a type alias. Later the code does not need to repeat the generic factory type parameters over and over again for each function call. With free standing functions this would be a bit more verbose. Especially if I have a lot of functions that would all need the factory.

fn priv_do<T: Factory>() -> T::Item {
    T::new_item()
}
fn exe<T: Factory>() -> T::Item {
    priv_do::<T>()
}

fn main() {
    println!("{:?}", exe::<Factory1>().get_kind());
    println!("{:?}", exe::<Factory2>().get_kind());
}

playground

Your Factory trait doesn't allow for stateful factories. Is there a reason for that?

Basically, why don't you have any values of these types?

Strawman

ah nice would this "Strawman"

....
struct Bar<T>(T);
....
let bar1 = Bar(Factory1);
...

still lead to a static dispatch?

Yeah, all the calls are still statically dispatched. And all the zero-sized values cost nothing to pass or hold in a variable because... well, because they're zero-sized.

(But it does give you the option to have dynamically dispatched factories, e.g. Box<Bar<dyn Factory>>.)

1 Like

the Problem that I need to solve is that I can not pass a factory instance into the caller context

Hmm.. why not? :thinking: I don't think I have a full picture of the problem you're trying to solve.

is there some runtime overhead of let bar1 = Bar(Factory1); as long as Factory1 is a unit struct / zero-sized?

Thx a lot for the help. :+1:

1 Like

No, no runtime overhead vs. the pure type version.

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.