Calling associated function of "tagged" struct

I have a noob question that I'm kind of stumped by. I'd like to subtly change the behavior of a struct's associated functions based on a ZST tag. Simple example:

struct A;
struct B;

pub struct Alien<AlienType>(AlienType);

impl<AlienType> Alien<AlienType> {
    fn greeting() -> String {
        format!("Greetings {}!", Self::my_name())
    }
}

impl Alien<A> {
    fn my_name() -> String {
        String::from("Alpha")
    }
}

impl Alien<B> {
    fn my_name() -> String {
        String::from("Beta")
    }
}

fn main() {
    println!("{}", Alien::<A>::greeting());
}

(playground)

Obviously what I'm looking for is

Greetings Alpha!

but instead I get what looks like a reasonable compilation error

  |
4 | pub struct Alien<AlienType>(AlienType);
  | --------------------------------------- function or associated item `my_name` not found for this
...
8 |         format!("Greetings {}!", Self::my_name())
  |                                        ^^^^^^^ function or associated item not found in `Alien<AlienType>`
  |
  = note: the function or associated item was found for
          - `Alien<A>`
          - `Alien<B>`

How can I make the generic struct's associated functions behave differently based on the tag in this way?

I'm also open to suggestions for how to restructure this more generally, and happy to share more context in that regard - though so far this overall structure seems closest to what I'm looking for.

EDIT: I'm aware that I could instantiate the struct with my_name set as a property, then make greeting() a method - but in principle in this case I'd like to avoid the need for instantiation.

Define a trait MyName that both A and B implement, and add a where clause to your Alien impl. (quick playground)

6 Likes

That’s awesome, thanks @erelde! That’s indeed what I was missing.

In general, whenever you need to abstract over types (in a way that you need to use some specific capability of those types), you will almost always need to use traits. Traits (like interfaces in some other languages) are the mechanism of specifying what a type can do, without having to know the concrete type itself.

And those capabilities must always and unconditionally be known when the body of a function (or really, any code that relies on them) is being compiled. Are you coming from a C++ background and assuming that Rust generics are like C++ templates? Well, they aren't – in Rust, the body of a generic function is fully type-checked, and the compiler will ensure that you only use items from traits that you explicitly declared (required) in the signature.

The reason for this is that i avoids post-monomorphization errors – once a generic function was typechecked successfully, it is 100% absolutely guaranteed not to generate compilation errors inside its own body later, regardless of what type it is instantiated with. (The compiler will simply refuse to instantiate such a generic with a non-conforming type that lacks the required bounds.) This makes the life of library writers much easier, because otherwise, it would be impossible to confidently ensure that truly all instantiations one envisioned will indeed compile.

However, in the above code, there is nothing to tell the Alien::greeting() function that Self will be a type that with a my_name() function! Even though locally, here is only Alien<A> and Alien<B>, it's in general possible to instantiate Alien with any future 3rd-party type of which the existence you (and the compiler at the time of type-checking your code) don't even know about. So in the impl Alien<AlienType> block, the compiler can't just assume that this type will be either A or B, because in the general case it won't be. Thus, it can't know that it has my_name(), unless that requirement/capability is explicitly indicated using a trait bound.

5 Likes

Thanks @H2CO3 you're totally right of course - and I appreciate the detailed explanation. I'm quite familiar with traits and in retrospect a trait is clearly what's required here. For some reason my brain didn't quite connect and reach for that particular tool when looking for a solution. I am indeed quite new to Rust and my muscle memory in that regard is still developing.

Even worse - Python - where Anything Goes™. The care that the Rust compiler puts into making sure you're not about to shoot yourself in the foot is quite remarkable - and a welcome change!

4 Likes