Function that can return any struct that implements a Trait


#1

I’m trying to create a function that takes a u32 id and returns a an object. I have a structs and their corresponding ids, they all implement a trait with a decode and encode function. I would like to pass the id to the function and have it decode based off of what id I gave it. How can I allow the function to return any struct that implements a trait? I initially had the idea of making a map, but I wasn’t sure how to put structs in there.

My current code was something along the lines of this, but I wasn’t sure how to get the true object type afterwards.

#[derive(Debug)]
pub struct TlManager {
    conn: File,
}

impl TlManager {
    fn serialize<T:Type>(&mut self, object: T) {
        object.serialize(self);
    }

    fn deserialize(&mut self, id: u32) -> Result<Box<Type>, BadIdError> {
        match id {
            0x997275b5 |
            0xbc799737 => Ok(box Bool::deserialize(id, self).unwrap() as Box<Type>),
            0x3fedd339 => Ok(box True::deserialize(id, self).unwrap() as Box<Type>),
            _ => Err(BadIdError{name: "Deserialize", recv: id})
        }
    }
}

#2

Perhaps you want to use this on Nightly?


#3

Thank you, that looks great! I’m planning to use this in a library I am making though, is it bad practice to use nightly for a library?


#4

It generally is, but that depends when the time you want to bring your library into general use is. AFAIK, many projects currently go the Box-way because it’s assumed to be forward-compatible with impl trait


#5

Ok, thanks for the info. Is there a description on the box-way in the wiki or a good link that you could share with me? I’ve sort of been putting pieces together from random SO links.


#6

Can you provide a little more context around the broader problem that you’re trying to solve?

When you say “get the true object type afterwards” that sounds to me that you plan to use some sort of switch statement over the returned value? That isn’t supported since Rust does not have Runtime Type Information. ie you won’t be able to write:

let t = manager.deserialize(id).unwrap();
if t instanceof Foo { // Runtime type lookup is not supported
    // ....
}

At a guess, it looks like you’re deserializing items from a file based on ID, and you want to support loading multiple different types. For this use case, I would suggest using an Enum (tagged Union type) instead of a Trait. Enums can have impls which may reduce your need to have your deserializer return a trait, and support matching to help switch between variants at runtime.

Example: https://play.rust-lang.org/?gist=9c0c25cc589dc9fd0fd1066a45f44961&version=stable&backtrace=0


#7

Note that there is one big difference between impl Trait and Box<Trait> - with impl Trait, the function can only return one type which implements the trait, every branch has to return the same type. With Box<Trait>, different branches can return different types. From the sample code I suspect you need the boxed trait, which is called a trait object.

In both cases, it is not possible to “downcast” the returned value back into the original type.


#8

But then how do I access values of the original type from the boxed value. Like type A has a string named cat. type A implements trait B. When I get the boxed B trait value, how do I access cat?

I’m actually creating a library for telegram (messaging service) and the api serializes objects and sends the over tcp. I want to deserialize them into rust objects. I get the id in the beginning, so I need to somehow find what object that corresponds to (and then I get strings and ints in the order of the attributes of the object). Could I use a map or something to get the struct by giving an id? There are a lot of objects, so an enum wouldn’t exactly be ideal.


#9

You need to expose the string through a method on B. You’re in the specific space where the difference between how polymorphism works in Rust and how it works in inheritance based object oriented languages makes Rust a bit less ergonomic. We’re working on features and design patterns that could ease this burden, but right now it is a bit tricky.


#10

That would be a lot easier to do, if I wasn’t trying to manage over 50 different A ish objects, they all have attributes. There has got to be an easier way than to just expose everything for everything.


#11

Probably what you want here though is to have the behavior pass through an entrypoint method on the trait and then everything happens in a context where you have the type.

For example, I have terminal application, which needs to parse escape codes. It works like this:

// The trait
trait Command {
     fn apply(&self, terminal: &mut Terminal);
}

// An example implementation
struct SetColor(Color);

impl Command for SetColor {
    fn apply(&self, terminal: &mut Terminal) {
        terminal.set_text_color(self.0);
    }
}

// Parsing
let command: Box<Command> = match escape_code {
    /* some pattern */  => Box::new(SetColor(color)),
}


// later
command.apply(&mut terminal);

#12

I sort of see where you are going with this, but I still feel like there should be an easier way to do this. I know an implementation of the telegram api in Golang was possible because you could return an object and then switch case for its type. There is a C implementation (not working anymore though) that I might be able to learn something from.

Also, with this. What would be the use for impl Trait then?


#13

The reason we can’t do what Go does is that types don’t carry any runtime type information, so we have basically no reflection capabilities. There’s some stuff you can do with the Any trait, maybe that will work for you.

Note however that a problem with reflection like this is that you lose a lot of the advantages of a strong type system. We can’t check that you have exhaustively covered all possible cases, for example, which could lead to surprising and difficult to trace bugs.

I think better solutions are possible, but we haven’t fully arrived at them yet.

Three reasons:

  1. Each lambda has a unique, anonymous type, so it isn’t possible to return an unboxed lambda without this change.
  2. The type of an iterator chain is like Map<Filter<Take<Iter<..>..>..>..>. This is both quite unwieldy, and if you change how how chain the iterators, the type changes. impl Iterator is easier.
  3. If you return an impl Trait, you can change the specific type you return without it being a breaking change. You can also hide other behavior that type implements, if you don’t want the client to be able to access that behavior right now.

#14

That makes sense, thanks for your help. I’ll look into the Any trait, hopefully I’ll eventually put something together that works for me.

P.S. congrats on getting on the language design team :wink:


#15

You said that you want to match type of object. How about enum?


#16

Because there are a lot of objects. I could use an enum, but there are just so many objects.


#17

You’ll be writing that many structs anyway, won’t you?


#18

Solid reasoning there. Might go with it


#19

The big question when it comes to enum versus trait is whether the set of types is open or closed; is your crate the sole source of all of these types (answer: use enum)? Or do you want others to be able to define more (answer: use a trait)?

You’ll find rustc has no fear of large enums.


#20

Yeah, it will be closed. But I realised a problem, some of the objects were going to be in enums already, but it doesn’t seem like I can put an enum in an enum.