Chain of transformations using generics

Say I want to model a chain (or tree) of transformations, where each layer can transform an input 'In' into an output 'Out' and have several subscribers that may further take an 'Out' as their 'In'...

I started with a simple trait like this, to enforce that subscribers must be compatible with this output (and they may transform it further to anything else):

trait Layer<In> {
  type Out;
  fn map(i:In) -> Self::Out;
  fn subscribe(l:Layer<Self::Out>);
}

But the compiler won't let me use Self::Out in a param, complaining that the "associated type Out must be specified"

What would be a proper way to model this?
Thanks :slight_smile:

This trait has several issues, but no one of them is connected to using Self::Out as parameter. Let's go through it:

  1. Compiler says that you can't use the trait in this case without specifying its associated type, i.e. in function subscribe you must accept something like Layer<Self::Out, Out = Next>. So the trait will look like this:
trait Layer<In> {
    type Out;
    fn map(i: In) -> Self::Out;
    fn subscribe<Next>(l: Layer<Self::Out, Out = Next>);
}
  1. The compiler, however, is not happy again:
error[E0038]: the trait `Layer` cannot be made into an object
 --> src/lib.rs:4:5
  |
4 |     fn subscribe<Next>(l: Layer<Self::Out, Out = Next>);
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Layer` cannot be made into an object
  |
  = note: method `map` has no receiver
  = note: method `subscribe` has no receiver

This means that you've used the deprecated trait object syntax.
Instead, you probably want to make subscribe function generic, i.e. have it compiled for every incoming layout as necessary. This is granted by a small change in singature:

trait Layer<In> {
    type Out;
    fn map(i: In) -> Self::Out;
    fn subscribe(l: impl Layer<Self::Out>);
}

Note the impl and the fact that the Next type is now unnecessary. More explicit way would be to write this:

fn subscribe<Arg: Layer<Self::Out>>(l: Arg);

i.e. add the generic parameter with trait bound, but semantically these two are equivalent.

  1. For now, it's unclear how this trait should be used. Look again at the previous error:
  = note: method `map` has no receiver
  = note: method `subscribe` has no receiver

This means that the trait is, essentially, a collection of functions, not attached to any object - only to the type. If this is OK, well, we've finished, but if you want to use them as object methods - you must add the receivers - argument referencing the object itself. This should possibly look like this:

trait Layer<In> {
    type Out;
    fn map(&self, i: In) -> Self::Out;
    fn subscribe(&mut self, l: impl Layer<Self::Out>);
}

I'm not sure if subscribe must really take the unique reference, or you'll go with some internal mutability, but in general - that's the way.

  1. The last but not the least. If you don't like the idea of multiple compiled instances of subscribe function for every input type, well, now - as we've added the receivers - we can go for dynamic dispatch, as you were trying at the start. But, instead of using deprecated syntax, we'll make the dynamic dispatch explicit by using the keyword dyn:
trait Layer<In> {
    type Out;
    fn map(&self, i: In) -> Self::Out;
    fn subscribe<Next>(&mut self, l: dyn Layer<Self::Out, Out = Next>);
}

Note, however, that we have to add the Next generic type again, so maybe this way won't give you any profit over statically dispatched one.

2 Likes

You can't use dynamic dispatch if your trait has any generic functions (like yours does). Also, you need some indirection, you can't just pass around values of dyn Layer<...> because dyn Trait is !Sized

2 Likes

Right, thanks - missed this point (not sure why I haven't recompile the last example).

1 Like

Thanks a lot for the detailed explanation.
The leap from "the trait cannot be made into an object" to the impl syntax is still mysterious for me but that's great help already and i'll toy with this some more.

.. having said that, I still feel i'm on the wrong track:
trying to subscribe 2 different layer impl for the same 'In' but different 'Out', the compiler complains about conflicting implementations.. every new line of rust is a new enigma for me :frowning:

That is a limitation in Rust. It can't tell that traits with different associated types are disjoint.

1 Like