4
votes

In my short Rust experience I ran into this pattern several times, and I'm not sure if the way I solve it is actually adequate...

Let's assume I have some trait that looks like this:

trait Container {
    type Item;
    fn describe_container() -> String;
}

And some struct that implements this trait:

struct ImAContainerType;
struct ImAnItemType;
impl Container for ImAContainerType {
    type Item = ImAnItemType;
    fn describe_container() -> String { "some container that contains items".to_string() }
}

This may be a container that has a knowledge about type of items it contains, like in this example, or, as another example, request which knows what type of response should be returned, etc.

And now I find myself in a situation, when I need to implement a function that takes an item (associated type) and invokes a static function of the container (parent trait). This is the first naive attempt:

fn describe_item_container<C: Container>(item: C::Item) -> String {
    C::describe_container()
}

This does not compile, because associated types are not injective, and Item can have several possible Containers, so this whole situation is ambiguous. I need to somehow provide the actual Container type, but without providing any container data. I may not have the container data itself at all when I invoke this function!

In search for a solution, I find the documentation for std::marker::PhantomData. It says:

PhantomData allows you to describe that a type acts as if it stores a value of type T, even though it does not.

This has to be the Rust's replacement for Haskell's Proxy type, right? Let's try to use it:

fn describe_item_container<C: Container>(container: PhantomData<C>, item: C::Item) -> String {
    C::describe_container()
}

let s = describe_item_container(PhantomData::<PhantomData<ImAContainerType>>, ImAnItemType);
println!("{}", s);

Compiling... Error:

error[E0277]: the trait bound `std::marker::PhantomData<ImAContainerType>: Container` is not satisfied

I ask #rust-beginners and get a response: PhantomData is not meant to be used that way at all! Also I got an advice to simply make a backward associated type link from Item to Container. Something like this:

trait Item {
    type C: Container;
}
fn describe_item_container<I: Item>(item: I) -> String {
    I::C::describe_container()
}

It should work, but makes things much more complicated (especially for cases when item can be placed in different container kinds)...

After a lot more experimentation, I do the following change and everything compiles and works correctly:

let s = describe_item_container(PhantomData::<ImAContainerType>, ImAnItemType);
println!("{}", s);

The change is ::<PhantomData<ImAContainerType>> to ::<ImAContainerType>.

Playground example.

It works, but now I'm completely confused. Is this the correct way to use PhantomData? Why does it work at all? Is there some other, better way to provide type-only argument to a function in Rust?


EDIT: There is some oversimplification in my example, because in that particular case it would be easier to just invoke ImAContainerType::describe_container(). Here is some more complicated case, when the function actually does something with an Item, and still requires container type information.

1
This does not compileseems to compile just fine. What am I missing?Shepmaster
@Shepmaster: This won't compile if you actually invoke that function from main().Sergey Mitskevich
I'm not sure if this is the right approach; what do you want to achieve? If you need a container with a generic member, why not just have some member T and e.g. use get_type_id() to get its concrete type when needed? Using an associated type doesn't seem like the way to go.ljedrz
@ljedrz Associated type gives compile-time guarantees. You are right, it can be checked dynamically during run-time with get_type_id(), but I prefer static restriction if possible.Sergey Mitskevich

1 Answers

7
votes

If you want to pass a type argument to a function, you can just do it. You don't have to leave it out to be inferred.

This is how it looks for your second example (playground):

fn pack_item<C: Container>(item: C::Item) -> ItemPacket {
    ItemPacket {  
        container_description: C::describe_container(),
        _payload: item.get_payload(),
    }
}

fn main() {
    let s = pack_item::<ImAContainerType>(ImAnItemType);
    println!("{}", s.container_description);
    let s = pack_item::<ImAnotherContainerType>(ImAnItemType);
    println!("{}", s.container_description);
}