Conditional Trait Implementations

I am having a hard time wrapping my head around how to design with traits for composition rather than inheritance. My design is heavily modular, and that may be complicating things. I thought I was on a roll until I discovered something I didn't expect:

  • impl declared anywhere in the code will get implemented, despite if it exists inside of a conditional if statement or even a function that never gets called.

I got very close to implementing inheritance in Rust. Here is a more detailed write-up with what I'm working on and code samples: https://stackoverflow.com/questions/45113763/rust-conditional-trait-implementation

Before I get around to studying composition methodologies and refactoring,

  1. Is there a fix for my case?
  2. Is there a composition methodology I should look at for my case?
  3. What are some good composition methodologies? Good resources for studying them?

Thanks :slight_smile: Open for any discussion!

EDIT:

The solution is Specialization, which plays much more nicely with Rust's paradigm.

I am surprised to hear that you can put impl blocks inside functions. An expansion of your question for others than the original poster: What is meaning of this? It seems like impl is very much meant to be a global property, thus the constraint that they can only be defined in the crate (module) that defines either the type or the trait. So what would it even mean to put impl into a function?

1 Like

You can actually put them in a macro -- without it automatically implementing them. Unfortunately, that didn't help me, because as soon as you try anything conditional, the same conflict problem exists.

trait A {
    fn method_a(&self);
}

struct Type1;

macro_rules! impl_conditionally { () => {

    impl A for Type1 {
        fn method_a(&self) {
            println!("A for Type1 from macro");
        }
    }

}}

fn main() {

    let t1 = Type1;
    t1.method_a();

}

error: no method named method_a found for type Type1 in the current scope

See, here the impl does not automatically implement like it would if it were inside a fn instead of a macro.

However, if there was an if or match statement in the macro to determine different implementations, it would implement them all upon calling. Same thing if you try to call the macro conditionally instead, like:

let c = false;

if c == true {
    impl_conditionally!();
}

it still gets implemented regardless. :smiley:

I think you are exactly right -- impl was designed to be a global feature, which means traits and methods have to be strictly composed. For me, what it means to put it into a function or macro is to allow less boilerplate as well as a whole lot more flexibility, even enough for reliable modularity. In my example, I showed how I get the result I wanted by manually implementing all the traits and methods, but a macro could do that in theory, since the end result is still valid Rust.

I understand the compiler is designed for integrity, and may not want that, at least not in that form. If there's a way to do what I want using composition with constraints and whatnot, I'd be interested.

Yes, because an impl is a static property of the code, considered at compile time regardless of what will execute. This is not like Python or any other dynamic language that can modify types at runtime.

For one, you can define items within function scope, like a nested fn, const, or even a new type. These are "global" more in the static sense, that their existence is not dependent on the runtime conditions of the function you define them in. From outside the function, there's no way to name the items within, so they're effectively scoped private regardless of whether they have pub.

Given that you can define types within a function, it also makes sense that you can impl those types. Coherence allows you to write such impls anywhere in the crate, as long as you can name it. Since you can only reach a fn's items from within, the impl has to go there too.

Since you need to be able to impl types within, there's just not much reason to limit you from also writing an impl for things from outer scopes. I mean, I'd probably frown on that in a code review, but the compiler doesn't need to have an opinion on it.

2 Likes

I am glad to have found out about specialization, which is a Rust nightly only feature right now and still unstable, but it makes much better sense than my original approach.

Thank you for clarifying the impl function situation. (My example was not trying to modify types at run-time per se, but to just produce the same implementations that I normally could hand-code, like a transpiler basically.)

So I have my inheritance working now using specialization, and already it is much more efficient, simple, and logical.

Ah, I had no idea you could define types within a function. I guess it's a reasonable way to modularize your code, and putting impl in the function then makes sense.

Regarding conditional (tricky) definitions of impl within macros, I think you could do that as long as your logic is all in the macro pattern. I've certainly used macros to reduce the tedium of writing lots of similar impls. Something like the following could give you some flexibility in defining impls. You couldn't use variable values in your "conditionals", but it does allow you (using stable) to define a family of implementations with somewhat less code repetition.

pub trait Greet {
    fn hello();
}

macro_rules! mkgreeter {
  ($t:ident, false, $name:expr) => {
    impl Greet for $t {
      fn hello() { println!("Goodbye {}!", $name); }
    }
  };
  ($t:ident, true, $name:expr) => {
    impl Greet for $t {
      fn hello() { println!("Hello {}!", $name); }
    }
  };
}

mkgreeter!(u8, false, "David");
mkgreeter!(u16, true, "Xoftware");

fn main() {
  u8::hello();
  u16::hello();
}
1 Like