How to use Traits as Generic type parameters?

Hello everyone,

I'm very new to rust and systems programming, so apologies if this is just fundamentally flawed thinking.

Permise

Suppose I have this trait -

trait Animal {
    fn say(&self);
}

And some implementers -

struct Dog {}

impl Animal for Dog {
    fn say(&self) {
        println!("Woof!");
    }
}


struct Cat {}

impl Animal for Cat {
    fn say(&self) {
        println!("Meow!");
    }
}

Interface analogy

Now how do I write code that is smart enough to work with either a Cat or a Dog?

If traits are anything like interfaces -- this should be easy!

let animal: Animal;    
animal = Dog { };
animal.say();

But no, rust doesn't like that.

error[E0308]: mismatched types
  --> src/main.rs:25:14
   |
25 |     animal = Dog { };
   |              ^^^^^^^ expected trait object `dyn Animal`, found struct `Dog`
   |
   = note: expected trait object `dyn Animal`
                    found struct `Dog`

error[E0277]: the size for values of type `dyn Animal` cannot be known at compilation time
  --> src/main.rs:24:9
   |
24 |     let animal: Animal;    
   |         ^^^^^^ doesn't have a size known at compile-time
   |
   = help: the trait `std::marker::Sized` is not implemented for `dyn Animal`
   = note: to learn more, visit <https://doc.rust-lang.org/book/ch19-04-advanced-types.html#dynamically-sized-types-and-the-sized-trait>
   = note: all local variables must have a statically known size
   = help: unsized locals are gated as an unstable feature

error[E0277]: the size for values of type `dyn Animal` cannot be known at compilation time
  --> src/main.rs:25:5
   |
25 |     animal = Dog { };
   |     ^^^^^^ doesn't have a size known at compile-time
   |
   = help: the trait `std::marker::Sized` is not implemented for `dyn Animal`
   = note: to learn more, visit <https://doc.rust-lang.org/book/ch19-04-advanced-types.html#dynamically-sized-types-and-the-sized-trait>
   = note: the left-hand-side of an assignment must have a statically known size


Box

A quick google leads me to Box<dyn T>, which isn't pretty, but certainly works -

let animal: Box<dyn Animal>;    
animal = Box::new(Dog { });
animal.say();
Woof!

Until one day...Generics :fearful:

Generics

A library gives me this nice function to create a couple of dogs -

fn get_n_dogs(n: usize) -> Vec<Dog> {
    let mut dogs = Vec::with_capacity(n);
    for _ in 0..n {
        dogs.push(Dog { });
    }
    dogs
}

But oh, now I can't use it without explicitly speficying the type of the animal!

let animals: Vec<Box<dyn Animal>>;
animals = get_n_dogs(5);
for animal in animals {
    animal.say();
}
error[E0308]: mismatched types
  --> src/main.rs:25:15
   |
25 |     animals = get_n_dogs(5);
   |               ^^^^^^^^^^^^^ expected struct `std::boxed::Box`, found struct `Dog`
   |
   = note: expected struct `std::vec::Vec<std::boxed::Box<dyn Animal>>`
              found struct `std::vec::Vec<Dog>`

Question

With this restriction, the (potentically huge) refactoring needed for a possible get_n_cats(5) seems too barbaric.

Is this really the end of the interface analogy? Is rust just too static to not let me write code like this?

Link to playground

Appendix

This can also be interpreted as "reverse generics", where the resolved type comes from the bottom-up, not top-down.

Something like this (hypothetical code) -

fn main() {
    let animals: Vec<Animal>; // eventually resolved to Vec<Dog> because of get_n_dogs()
    animals = top();
}

fn top<T: Animal>() -> Vec<T> {
    return bottom();
}

fn bottom<T: Animal>() -> Vec<T> {
    return get_n_dogs();
}

Dog and Box<dyn Animal> are represented in very different ways in memory, so you have to explicitly convert it.

Here's one way to do so:

fn as_boxed_animals<'a, T: Animal + 'a>(vec: Vec<T>) -> Vec<Box<dyn Animal + 'a>> {
    vec.into_iter()
        .map(|a| Box::new(a) as Box<dyn Animal>)
        .collect()
}

fn main() {
    let animals: Vec<Box<dyn Animal>>;
    animals = as_boxed_animals(get_n_dogs(5));
    for animal in animals {
        animal.say();
    }
}

Generally Rust is not a big fan of silent expensive conversions, but it is a fan of allowing you to use cheap representations such as Vec<Dog> rather than the more expensive ones such as Vec<Box<dyn Animal>> when you don't need dynamic dispatch.

3 Likes

While that workaround is sweet, I'm pretty sure it won't always work.

For e.g. - what about types that don't let you access their innards directly? - like a Vec lets you access the Animal stored in it, but a websocket library won't let me access its internal stream and just swap it out for a boxed version -

use websocket::ClientBuilder;

let client: Client<Stream>;
client = ClientBuilder::new("ws://...")?
    .connect_insecure()?;

Its not very clear what the exact type of the client variable must be, because the Client<S>'s generic parameter S changes based on the ClientBuilder configuration!

Besides, manually building a Boxed version of everything looks like a lot more work that just specifying the type exactly and embracing the refactor :smile:.

I think you're imagining a problem where none really exists.

In your original example the concrete type is always known. It's not a problem. You don't have to convert a Dog to a dyn Animal to use Animal behavior on it.

let animals = get_n_dogs(5);
for animal in animals {
    animal.say();
}

This is no less general than the original code. The type is always known concretely, anyway, so there's no point in obfuscating it. And the only "refactoring" you need to replace get_n_dogs with get_n_cats is exactly that... just those 3 characters.

I don't really get what you're going for with the "reverse generics" but it sounds like impl Animal:

fn main() {
    let animals = top();
}

fn top() -> Vec<impl Animal> {
    bottom()
}

fn bottom() -> Vec<impl Animal> {
    get_n_dogs(5)
}

impl Trait has some limitations, but I'd need some more information about what you're actually doing to comment on whether those are relevant or whether impl Trait even solves the problem at all.

3 Likes

Apologies - The refactoring is not visible in this small example, but think about a large codebase where I'm passing around animals, storing in them in structs and what not.

In that case, the refactor becomes huge as soon as I change the 3 characters from get_n_dogs to get_n_cats.

impl Trait is super cool, and it solves the problem with functions, but it doesn't let me work with structs :frowning:

E.g. -

struct Session {
    client: websocket::sync::Client<impl websocket::sync::Stream>
}
error[E0562]: `impl Trait` not allowed outside of function and inherent method return types
   --> src/main.rs:139:37
    |
139 |     client: websocket::sync::Client<impl websocket::sync::Stream>,
    |                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

or

struct Session {
    animals: Vec<impl Animal>
}
error[E0562]: `impl Trait` not allowed outside of function and inherent method return types
  --> src/main.rs:27:18
   |
27 |     animals: Vec<impl Animal>
   |                  ^^^^^^^^^^^

Thanks for entertaining my confused mind.

Hey, well in this case your Session would become generic like so:

struct Session<T: Animal> {
    animals: Vec<T>
}

This will always limit the Vec to only be able to store the same animal. If you require to store several kinds of animals in the same Vec the only way - as far as I'm aware - is to use trait objects:

struct Session {
    animals: Vec<Box<dyn Animal>>,
}
struct Session<T: Animal> {
    animals: Vec<T>
}

Hmm, I know that this looks good, but it simply delegates the responsibility of specifying the concrete Animal type onto the creator of Session. This isn't what I'm after.

Storing several kinds of animals is not the goal here either.

The goal is to let the caller of function / creator of struct be free from the responsibility of specifying a concrete type for animal. Instead the function itself gets to do that.

This is exactly the problem impl Trait solves.

I guess I'll just have to wait for 2071-impl-trait-existential-types to be accepted.

Thanks for taking interest.

What are you after though?

The caller is not the creator of the struct in any of these examples.

Note that functions can return wrapped impl Traits, e.g. -> Session<impl Animal>.

1 Like

Would it be possible to write code that (1) you want to be able to write and (2) currently does not compile ?

I think this would greatly clarify to us what you are trying to achieve. (From personal experience, this forum is also amazing at debugging problems of the form "how do I make this Rust-looking code compile"?)

1 Like

Whoops, looks like its getting too confusing here. Maybe this will clear things up -

Here's why generics don't work -

struct Session<A: Animal> {
    my_animal: A,
}

impl<A: Animal> Session<A> {
    fn new() -> Self {
        Self { my_animal: Dog {} }
    }
}
error[E0308]: mismatched types
  --> src/main.rs:45:27
   |
43 | impl<A: Animal> Session<A> {
   |      - this type parameter
44 |     fn new() -> Self {
45 |         Self { my_animal: Dog {} }
   |                           ^^^^^^ expected type parameter `A`, found struct `Dog`
   |
   = note: expected type parameter `A`
                      found struct `Dog`

I want new() to be able to resolve the concrete type for A.

Sorry, I still do not understand what you want to achieve.

The code above is issuing conflicting requirements:

  • the function type signature says: for any A that satisfies Animal, Session<A>::new() will return a Session<A>

  • but the function body always returns a Session<Dog>

===

Can you expand on this ?

I might have made things unnecessarily abstract and shot myself in the foot with Vec. This is what I was actually trying to do -

And yes, generics don't really work well for this -

use anyhow::Result;
use websocket;

fn main() -> Result<()> {
    let session = Session::new()?;
    Ok(())
}

struct Session<T: websocket::sync::Stream> {
    client: websocket::sync::Client<T>,
}

impl<T: websocket::sync::Stream> Session<T> {
    fn new() -> Result<Self> {
        let client = websocket::ClientBuilder::new("ws://...")?.connect_insecure()?;

        Ok(Self { client })
    }
}
error[E0282]: type annotations needed for `Session<T>`
 --> src/main.rs:5:19
  |
5 |     let session = Session::new()?;
  |         -------   ^^^^^^^^^^^^ cannot infer type for type parameter `T`
  |         |
  |         consider giving `session` the explicit type `Session<T>`, where the type parameter `T` is specified

error[E0308]: mismatched types
  --> src/main.rs:17:19
   |
13 | impl<T: websocket::sync::Stream> Session<T> {
   |      - this type parameter
...
17 |         Ok(Self { client })
   |                   ^^^^^^ expected type parameter `T`, found struct `std::net::TcpStream`
   |
   = note: expected struct `websocket::sync::Client<T>`
              found struct `websocket::sync::Client<std::net::TcpStream>`

error: aborting due to 2 previous errors

I realize that I can just specify the concrete type for Stream, which is TcpStream here.

But a refactor is involved when I switch from connect_insecure() to connect_secure() because in that case the TcpStream changes to TlsStream<TcpStream>. Similarly, connect() returns a different concrete type - Box<dyn NetworkStream + Send>.

I am trying to make my code work irrespective of the concrete type of Stream returned by the ClientBuilder.

I am reading https://docs.rs/websocket/0.26.2/websocket/client/builder/struct.ClientBuilder.html

pub fn connect_insecure(&mut self) -> WebSocketResult<Client<TcpStream>>
pub fn connect_secure(
    &mut self,
    ssl_config: Option<TlsConnector>
) -> WebSocketResult<Client<TlsStream<TcpStream>>>

I understand that connect_insecure() and connect_secure() returns different types.

It sounds like you want to write a function in such a way so that

(1) it compiles and
(2) when you do connect_secure <-> connect_insecure, it still compiles

^-- Is this iinterpretation correct?

1 Like

Yes, absolutely!

I should have started with this, sorry for the trouble.

Well, if you don't need to put the new function as implementation for the Session struct you could also do it like so:

use anyhow::Result;
use websocket;

fn main() -> Result<()> {
    let session = Session::new()?;
    Ok(())
}

struct Session<T: websocket::sync::Stream> {
    client: websocket::sync::Client<T>,
}

fn create_session() -> Result<Session<impl websocket::sync::Stream>> {
   let client = websocket::ClientBuilder::new("ws://...")?.connect_insecure()?;

   Ok(Self { client })
}

So you are free to pick another client builder inside this function later on.

1 Like

Great thinking. Exactly what I was looking for. I hope one day we can use impl Trait in structs and not have to do these gymnastics.

Slight correction -

use anyhow::Result;
use websocket;

fn main() -> Result<()> {
    let session = create_session()?;
    Ok(())
}

struct Session<T: websocket::sync::Stream> {
    client: websocket::sync::Client<T>,
}

fn create_session() -> Result<Session<impl websocket::sync::Stream>> {
    let client = websocket::ClientBuilder::new("ws://...")?.connect_insecure()?;

    Ok(Session { client })
} 

Although in this example it is just a matter of "style preference", the problem does indeed arrive when unnameable types are involved: let's image someone wants to have:

struct Session<F : Fn()> {
    f: F,
}

impl Session<?> {
    fn new () -> Session<impl Fn()>
    {
        Session { f: || println!("Demo"), }
    }
}
  • your code example is similar to this situation, except that you could name the type but you don't want to, to ease refactoring.

  • I already covered this use case in an old post: Embedding Trait inside of Struct

In that case, the clean solution would be to use existential types:

type WebSocketStream = impl Stream...;
// attach the actual type with a constructor:
fn mk_websocket_stream (...) -> WebSocketStream
{
    connect_secure(...)
    /* could be refactored as
    connect_insecure(...) // */
}

/// And now we can do:
struct Session { // <- no longer generic!
    client: WebSocketStream.
}

impl Session {
    fn new (...) -> Self
    {
        Self { client: mk_websocket_stream(...) }
    }
}

Now, on stable we don't have this, so we need to be a bit more hacky creative :smile::

The idea will be to have Session::new() return a Session<impl Stream>, although this return type cannot be Self, since there is no way to impl for ...<impl Stream> on stable Rust.

This isn't a problem, as long as we don't introduce an unused generic type parameter.

So, for instance, we can do:

pub
struct Session<T> { // <- no trait bound **yet**
    client: T,
}

impl Session</* what do we put here? */> {
    pub
    fn new (...) -> Session<impl Stream>
    {
        let client = ...;
        Self { client }
    }
}

/// CLASSIC IMPLS HERE:
// generic impl just to be able to cover the above `impl Stream` unnameable type.
impl<T : Stream> Session<T> {
    ...
}

So, the only question remaining is:

what type do we use in /* what do we put here */?

And the answer is: whatever you want, as long as it is not generic:

/// Even a type as dumb as `()` works.
impl Session<()> {
    fn new (...) -> Session<impl Stream> { ... }
}

/// Using `dyn Trait` for something that becomes `impl Trait` may "read / look better"
impl Session<dyn Stream> { // <- But you need to be able to use a `?Sized` type param
   ...
}

// The longer but better looking solution
use private::impl_Stream;
mod private {
    #![allow(nonstandard_style)]
    pub
    struct impl_Stream;

    /// If you have added a `: Stream` boubnd even at the struct definition
    /// you will need a dummy impl of Stream for this dummy type;
    impl Stream for impl_Stream { ... unreachable!() ... unreachable!() ... }
}
/// so that you can do:
impl Session<impl_Stream> {
    fn new (...) -> Stream<impl Stream> { ... }
}
6 Likes

Consider me a fan..
This compiles!

#![feature(type_alias_impl_trait)]

use anyhow::Result;
use websocket;
use websocket::sync::{Client, Stream};
use websocket::ClientBuilder;

type WsStream = impl Stream;
type WsClient = Client<WsStream>;

struct Session {
    client: WsClient,
}

impl Session {
    fn new() -> Result<Self> {
        Ok(Self {
            client: Self::create_client()?,
        })
    }

    fn create_client() -> Result<WsClient> {
        Ok(ClientBuilder::new("ws://...")?.connect_insecure()?)
    }
}

fn main() {
    let s = Session::new();
}