2

I have a similar use-case to the one described here, but it's slightly different in that my solution can't replace the generic method with a non-generic method. Here's the code that I have (Rust Playground):

use serde::{de::DeserializeOwned, Serialize};
use serde_json;

trait Serializer { 
    fn serialize_data<V>(&self, data: &V) -> Result<String, String> where V: Serialize;

    fn deserialize_data<V>(&self, ser_data: &str) -> Option<V> where V: DeserializeOwned;
}

struct JsonSerializer { 
    x: i32 // some member I need to store
}

impl JsonSerializer {
    fn new() -> JsonSerializer {
        JsonSerializer { x: 1 }
    }
}

impl Serializer for JsonSerializer {
    fn serialize_data<V>(&self, data: &V) -> Result<String, String> where V: Serialize {
        match serde_json::to_string(data) {
            Ok(ser_data) => Ok(ser_data),
            Err(err) => Err(err.to_string())        
        }
    }

    fn deserialize_data<V>(&self, ser_data: &str) -> Option<V> where V: DeserializeOwned {
        match serde_json::from_str(ser_data).unwrap() {
            Ok(val) => Some(val),
            Err(_) => None
        }
    }
}



// I may want to have more serializer objects like 
// YamlSerizlier, BincodeSerializer and so on...
// ...

struct MyMainObject {
    serializer: Box<Serializer>
}

impl MyMainObject {
    fn new() -> MyMainObject {
        MyMainObject { serializer: Box::new(JsonSerializer::new()) }
    }

    fn do_something(&self) {
        println!("{}", self.serializer.serialize_data(&1));
        println!("{}", self.serializer.serialize_data(&String::from("MY STRING")));
    }
}

fn main() {
    let my_main_object = MyMainObject::new();
    my_main_object.do_something();
}

As described in the previous question, when compiling this code I get an error the trait `Serializer` cannot be made into an object because it has generic methods:

   Compiling playground v0.0.1 (/playground)
error[E0038]: the trait `Serializer` cannot be made into an object
  --> src/main.rs:42:5
   |
42 |     serializer: Box<Serializer>
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Serializer` cannot be made into an object
   |
   = note: method `serialize_data` has generic type parameters
   = note: method `deserialize_data` has generic type parameters

But in my case I want these methods to stay generic so I can serialize/deserialize any type of data.

So my question is how to keep the dynamic dispatch pattern and still make it work, meaning I want to a Serializer trait member in MyMainObject that I can initialize with any type of serializer object (Json, Yaml, etc.), and then call serializer.serialize_data() or serializer.deserialize_data() inside of MyMainObject.

If this is not possible, what is the best alternative you can suggest?

EDIT:

I need a solution that would work for different kind of serializers, at list those ones:

seladb
  • 852
  • 1
  • 13
  • 29
  • [This library](https://crates.io/crates/typetag) was announced some time ago on Rust's subreddit. I have not looked too much into it, but it may be what you want – Michail Jan 25 '19 at 18:22
  • If I understand it correctly, this library enables serialization / deserialization of trait objects, but what I'm looking for is a way to implement different serializers under the same trait, or at least a design close to that – seladb Jan 26 '19 at 08:26
  • Oh, right, should've read the question a bit more carefully, sorry). Well, there's always the enum approach, but that's a lot of boilerplate and not really well extensible. – Michail Jan 26 '19 at 11:07
  • Can you please elaborate on this option? – seladb Jan 31 '19 at 21:02
  • Wrapping all of your serializers in an enum, and using the enum instead of `Box`. The main downside is that you're losing the flexibility trait objects offer - whenever you want to add support for a new serializer, you need to add a variant to the enum, and the users of your library (if it is one) can't just plug in their own serializers without similar workarounds. But it works for generic things. – Michail Jan 31 '19 at 21:53
  • Can you please add an answer with the code? I'm not sure I'm 100% following you – seladb Feb 01 '19 at 06:10

2 Answers2

2

You can't use non-object-safe traits with dynamic dispatch; the object safety rules are specifically about the things that prevent dynamic dispatch.

There are sometimes workarounds for specific scenarios. They are usually complex. But for serde specifically, there's the erased_serde crate, because you're not the first one with this problem.

Sebastian Redl
  • 69,373
  • 8
  • 123
  • 157
  • Thanks! will this library work with [bincode](https://github.com/TyOverby/bincode) and [serde_yaml](https://github.com/dtolnay/serde-yaml) as well? – seladb Jan 25 '19 at 07:43
  • @fx23 I don't know, I haven't used it myself. But I see no reason why it shouldn't. – Sebastian Redl Jan 25 '19 at 09:18
  • serde_json and serde_cbor contain a `Serializer` object that implements the `serde::ser::Serializer` trait. erased_serde counts on that. However bincode and serde_yaml don't have this object, so I don't think they can be used with erased_serde – seladb Jan 25 '19 at 09:47
  • I assume you're the one who opened the bug on erased_serde. You might be interested in these issues: https://github.com/dtolnay/serde-yaml/issues/44 https://github.com/TyOverby/bincode/issues/242 – Sebastian Redl Jan 25 '19 at 10:13
  • Thanks! Apparently this library can't be used with `bincode` and `serde_yaml`. I understand this design won't work in Rust. Can you suggest an alternative design which will be as close as possible to what I want? – seladb Jan 25 '19 at 10:30
2

Note

The following is not a good long-term solution, it is merely a workaround. A proper way to do what you want is to figure out and implement a method to reconcile bincode and serde_yaml with erased_serde. But if you need it to work right now, here's

the gist

Basically, you can use enums to write a poor man's dynamic dispatch. It looks more or less like this (I've simplified and omitted some things):

struct JsonSerializer();
struct YamlSerializer();

trait Serializer {
    fn serialize<V>(&self, thing: &V) -> ();
}

impl Serializer for JsonSerializer {
    fn serialize<V>(&self, thing: &V) -> () {
        println!("json");
    }
}

impl Serializer for YamlSerializer {
    fn serialize<V>(&self, thing: &V) -> () {
        println!("yaml");
    }
}

// That's what we'll be using instead of Box<dyn Serializer>
enum SomeSerializer {
    Json(JsonSerializer),
    Yaml(YamlSerializer),
}

impl SomeSerializer {
    pub fn serialize<V>(&self, thing: &V) -> () {
        match self {
            SomeSerializer::Json(ser) => ser.serialize(thing),
            SomeSerializer::Yaml(ser) => ser.serialize(thing),
        }
    }
}

Here's how you'd use it (except you'd probably want actual constructor functions here):

pub fn main() {
    let thing = 2;
    let json = SomeSerializer::Json(JsonSerializer());
    let yaml = SomeSerializer::Yaml(YamlSerializer());
    json.serialize(&thing);
    yaml.serialize(&yaml);
}

This has serious drawbacks (see below), but it does allow you to pack something that has generic methods into a unified interface.

The problems

The chief problem with this approach is that it's hard to add new serializers to the setup. With Box<dyn Serializer> all you need to do is to impl Serializer for something. Here you have to add a variant to the enum and pattern match on it in all relevant methods. This is inconvenient in the crate where SomeSerializer is defined, and impossible in other crates. Moreover, adding a variant to a public enum is a breaking change, which downstream crates may not exactly welcome. There are ways to ameliorate this to some extent:

Hide SomeSerializer

It doesn't really make sense for SomeSerializer to be public. Ability to pattern match on it has very little benefit, and it being public limits what you can do to it without breaking things downstream. The usual solution is to put it in an opaque struct and export that, leaving the enum itself hidden:

pub struct VisibleSerializer(SomeSerializer);

Still use the trait

You can't extend SomeSerializer with extra serializers in other crates. You can continue mounting more enum layers on it (and that is both unfortunate and ugly), but then no function in the original crate will accept such construct. This can be helped: rather than making serialize an inherent method of SomeSerializer, implement Serializer for it, and make all functions that would use SomeSerializer generic and accept a T: Serializer. Suddenly all of downstream crates can add a serializer they want to the setup.

Special-case only special cases

Having more than three of four serializers wrapped this way is kind of ridiculous, not to mention awkward. However, if the majority of serializers you want to work with are actually erased_serde-compatible, you can have a kind of catch-all enum variant for them in SomeSerializer, and have separate variants only for the incompatible ones:

enum SomeSerializer {
    Whatever(Box<dyn erased_serde::Serializer>),
}
Michail
  • 1,843
  • 1
  • 17
  • 21