-2

I am trying to implement a generic structure with a bunch of fields, where each of the field types should know about the exact type of the whole structure. It's a sort of strategy pattern.

pub struct Example<S: Strategy<Example<S, D>>, D> {
    pub s: S,
    pub a: S::Associated,
    pub data: D,
}
pub trait Strategy<T> {
    type Associated;
    fn run(&self, &T);
}
pub trait HasData {
    type Data;
    fn data(&self) -> &Self::Data;
}

impl<S: Strategy<Self>, D> Example<S, D> {
//               ^^^^
// the complex code in this impl is the actual meat of the library:
    pub fn do_it(&self) {
        self.s.run(self); // using the Strategy trait
    }
}
impl<S: Strategy<Self>, D> HasData for Example<S, D> {
    type Data = D;
    fn data(&self) -> &D {
        &self.data
    }
}

I was planning to then instantiate the generics from the above "library":

pub struct ExampleStrat;
pub struct ExampleData;

impl<E: HasData<Data = ExampleData>> Strategy<E> for ExampleStrat {
    type Associated = ();
    fn run(&self, e: &E) {
        let _ = e.data();
        // uses ExampleData here
    }
}
let example = Example {
    s: ExampleStrat,
    a: (),
    data: ExampleData,
};
example.do_it();

In my actual code I've got quite a few different "strategies" and also multiple data fields, so the Example type has an impressive list of generics, and I'm happy if the library user doesn't need to be explicit about them (or not often at least) and instead can just use the HasData trait (with its associated types, not generic type parameters).

If there was no type bound in struct Example<S, D>, this would actually work (surprisingly) fine, much better than I has initially expected (after fighting with Self in the struct bounds). However it is recommended to duplicate the impl trait bounds on the struct when the struct is only supposed to be used with the constrained types, and in my case I actually need them to be able to use the Associated type for the a field.

Now the compiler is complaining

error[E0275]: overflow evaluating the requirement `main::ExampleStrat: Strategy<Example<main::ExampleStrat, main::ExampleData>>`
  --> src/main.rs:42:9
   |
42 |         a: (),
   |         ^^^^^
   |
   = note: required because of the requirements on the impl of `HasData` for `Example<main::ExampleStrat, main::ExampleData>`
   = note: required because of the requirements on the impl of `Strategy<Example<main::ExampleStrat, main::ExampleData>>` for `main::ExampleStrat`

How can I solve this? Am I trying to do something that is not possible, am I doing it wrong, or is it supposed to be possible but I am falling prey to a compiler bug? Is my complete design flawed?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • In general, it's not good practice to put trait bounds on a struct or trait's type parameters. If you move them to the impls instead, then you will see more clearly why the cyclic dependencies are occurring. – Peter Hall Jun 02 '18 at 14:12
  • For example: https://play.rust-lang.org/?gist=0cadebe7e494d464e976b860c3a22c71&version=stable&mode=debug – Peter Hall Jun 02 '18 at 14:15
  • @PeterHall I do see a cyclic dependency, but I don't see why it can't be resolved. – Bergi Jun 02 '18 at 14:16
  • @PeterHall Doesn't [this (without using the associated type)](https://play.rust-lang.org/?gist=30db42f406ef81ea745eaa18d965f794&version=stable&mode=debug) do essentially the same? – Bergi Jun 02 '18 at 14:31
  • Yes. But you've lost the generic type in the main struct. Presumably you didn't want to hard-code that as `()`? – Peter Hall Jun 02 '18 at 14:34
  • @PeterHall Yes, that's what the question is about: can I make use of an associated type there? – Bergi Jun 02 '18 at 15:00
  • Sorry, this example is too abstract for me. I have read a little about CRTP and it seems to be a *very* C++ specific pattern. For example, [this answer](https://stackoverflow.com/a/26718782/3650362) describes using it to do something that seems [trivially solvable with traits alone](https://play.rust-lang.org/?gist=05bbafb5cf2e01b527a9a845dd5eafc8&version=stable&mode=debug) (no generics in sight). – trent Jun 02 '18 at 16:51
  • @trentcl OK, I guess this isn't very much like the CRTP from C++. It's just "curiously recurring" in that `.s` is an `Strategy`, so `s.run` "knows" about the parent type and can take it as a parameter (though I am using composition, not inheritance). – Bergi Jun 02 '18 at 17:30
  • I don't think your example justifies parameterizing `Strategy` over the "master" type rather than just the data type. [For example, this works fine](https://play.rust-lang.org/?gist=1f682402286cbca96ddcebe7c466517e&version=stable&mode=debug) and I think the only thing it gives up is the ability to have `Strategy` trait objects. You can get even that back at the cost of double dynamic dispatch (use `&HasData` instead of `&impl HasData`). A more concrete example might help. – trent Jun 02 '18 at 18:20
  • @trentcl Thanks, moving the `HasData` constraint to a type parameter at `run` (not the complete `Strategy`) is a good idea. For some reason I believed it was necessary to have the `impl Example` reflect that type bound. Can you write an answer with that? – Bergi Jun 02 '18 at 18:32
  • Sure! I added some other suggestions too, but because I haven't completely grasped your design, they might not apply. – trent Jun 02 '18 at 20:51

2 Answers2

2

First of all, everything becomes a lot clearer if you avoid putting trait bounds on definitions of structs and traits. When things get complicated, the constraints are at least solved from the same direction.

pub struct Example<S, D, A> {
    pub s: S,
    pub a: A,
    pub data: D,
}

pub trait Strategy<T> {
    type Associated;
    fn run(&self, &T);
}

pub trait HasData {
    type Data;
    fn data(&self) -> &Self::Data;
}

impl<S, D, A> Example<S, D, A>
where
    S: Strategy<Self, Associated = A>,
{
    pub fn do_it(&self) {
        self.s.run(self);
    }
}

impl<S, D, A> HasData for Example<S, D, A>
where
    S: Strategy<Self, Associated = A>,
{
    type Data = D;
    fn data(&self) -> &D {
        &self.data
    }
}

Your implementation of Strategy for ExampleStrat looks like this:

impl<E: HasData<Data = ExampleData>> Strategy<E> for ExampleStrat {
    type Associated = ();
     // ...
}

What this means is that you are defining it for all possible qualifying types E. The type-checker can now only look at the trait bounds, which are again generic and only expressed in terms of other traits, which use each other as bounds, so the type-checker gets into a cycle. Put a block in the cycle by giving it a concrete type, which you know.

pub struct ExampleStrat;
pub struct ExampleData;

impl Strategy<Example<ExampleStrat, ExampleData, ()>> for ExampleStrat {
    type Associated = ();
    fn run(&self, e: &Example<ExampleStrat, ExampleData, ()>) {
        let _ = e.data();
        // uses ExampleData here
    }
}

fn main() {
    let example = Example {
        s: ExampleStrat,
        a: (),
        data: ExampleData,
    };
    example.do_it();
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Peter Hall
  • 53,120
  • 14
  • 139
  • 204
  • Thanks, I understand. This is what I was going with originally, but my library is supposed to use multiple kinds of strategies (not just the `Strategy` trait) and there might be multiple implementations (not just the `ExampleStrat` for each of those strategy kinds), so that in `main` an `Example` could be combined from the implementations pretty arbitrarily. So the `ExampleStrat` definition would look more like `impl Strategy> for ExampleStrat`, and that's what I wanted to avoid using a `trait`. Is that not possible? – Bergi Jun 02 '18 at 15:11
  • This is just saying that `ExampleStrat` is only a valid `Strategy` when used with an `Example` and with `ExampleData`. You can create other strategies for `Example`, and you could declare different `impl`s of `Strategy` for `ExampleStrat`, so it could work with other targets. – Peter Hall Jun 02 '18 at 15:25
  • Overall, it looks like you are trying to port an OO pattern directly from Java or C++. Usually that will not result in nice idiomatic Rust. – Peter Hall Jun 02 '18 at 15:27
  • Yes, exactly that is what I want to say, however if possible not with the full load of generic parameters A-Z on every `impl`. I know that my `ExampleStrat` strategy implementations work only for the `Example` type, however more or less "any" `Example` type regardless of its type parameters. – Bergi Jun 02 '18 at 15:32
  • Yes, I was trying to port the Curiously Recurring Template Pattern from C++ to Rust - in each of my microcontroller applications I will need only one instance of one specific type (an `Example` instantiated with the strategies appropriate for the current application), but I wanted to define my library methods on `Example` generically. What would the idiomatic Rust pattern be for this? – Bergi Jun 02 '18 at 15:37
  • 1
    I guess maybe I should ask a separate question about that, and leave this one concerning only the error message. – Bergi Jun 02 '18 at 16:22
1

If the following impl is characteristic for Strategy, then it might be parameterized on the wrong thing. (I'm going to ignore the associated type for this answer, because the example doesn't use it.)

impl<E: HasData<Data = ExampleData>> Strategy<E> for ExampleStrat {
    fn run(&self, e: &E) {
        let _ = e.data();
        // uses ExampleData here
    }
}

You could instead parameterize Strategy over D -- breaking the impl dependency cycle -- and parameterize only the run method over E.

pub trait Strategy<D> {
    fn run(&self, &impl HasData<Data = D>);
}

impl Strategy<ExampleData> for ExampleStrat {
    fn run(&self, e: &impl HasData<Data = ExampleData>) {
        let _ = e.data();
        // uses ExampleData here
    }
}

fn run<E: HasData<Data = ExampleData>>(&self, e: &E) is another way to define run that is the same for this purpose. Here is a full example.

A potential drawback of this approach is that run can't be called through a Strategy trait object, because it has to be monomorphized for any type that implements HasData. But the HasData trait doesn't seem to do much in this impl: the only thing it can do is return an internal reference, and once you have it, there's no point in using it again. Maybe run could just take a &D reference?

pub trait Strategy<D> {
    fn run(&self, &D);
}

impl Strategy<ExampleData> for ExampleStrat {
    fn run(&self, _: &ExampleData) {
        // uses ExampleData here
    }
}

To be sure, now you have to call self.s.run(self.data()) in do_it, but this doesn't cost you in flexibility over the original version, in which, had it worked¹, you could only call Strategy<E>::run with an argument of type &E.

In fact, the whole HasData trait seems unnecessary to me: it's always implemented by the same type whose implementation calls it, so aside from the minor convenience of passing self instead of self.data, it doesn't elevate the level of abstraction inside the do_it method. So it seems to me effectively the same thing to delete HasData entirely and let Example know how to call Strategy::run with the right reference; it has to, anyway. (However, it's possible I merely lack imagination.)

Any of these solutions ought to handle adding an associated type to Strategy, but without knowing how it will be used, it's hard to say for sure.

¹It could be made to work in some future version of the compiler, with sufficiently smart type checking.

trent
  • 25,033
  • 7
  • 51
  • 90
  • "*the `HasData` trait doesn't seem to do much in this `impl`*" - true, I've simplified it for the question. It contains many getters and setters in the `Example` struct, and the different `Strategy` implementations use different subsets of them. I would have written the `Strategy` trait so that `run` takes an `Example<…>` ((mutable) reference) directly, the *inconvenience* is that this requires each `impl Strategy` to list the myriads of type parameters and their bounds. (Part of this might be solved with [RFC2089](https://github.com/rust-lang/rfcs/blob/master/text/2089-implied-bounds.md)) – Bergi Jun 02 '18 at 21:28
  • @Bergi I figured there might be more to it. But this might be a case where it's better to bite the bullet and write the bounds. – trent Jun 02 '18 at 22:42