Unable to provide default trait implementation

Hi everyone,

I'm relatively new to Rust, so this might be a basic question, but I've been stuck on this for hours and could really use some help.

I have the following code example, which compiles and works as intended. It allows me to create new structs that implement the Computation trait, and these structs automatically inherit the methods from the SerializableComputation trait.

pub trait Computation: SerializableComputation  {
    fn run(&mut self, input: ComputationInput) -> ComputationOutput;
}

pub trait SerializableComputation: Send + Sync  {
    fn update_computation(&mut self, map: &HashMap<String, Value>);
    fn serialize_computation(&self) -> Vec<u8>;
    fn deserialize_computation(&self, data: &[u8]) -> Box<dyn SerializableComputation>;
}

impl<T> SerializableComputation for T 
where
    T: Computation 
        + Serialize 
        + for<'de> Deserialize<'de> 
        + Clone 
        + Send 
        + Sync 
        + 'static,
{
    fn update_computation(&mut self, map: &HashMap<String, Value>) {
        let mut value = to_value(self.clone()).expect("Failed to convert to serde_json::Value");

        if let Value::Object(ref mut obj) = value {
            for (key, val) in map {
                obj.insert(key.clone(), val.clone());
            }
        }

        *self = from_value(value).expect("Failed to convert from serde_json::Value");
    }

    fn serialize_computation(&self) -> Vec<u8> {
        bincode::serialize(self).expect("Failed to serialize")
    }

    fn deserialize_computation(&self, data: &[u8]) -> Box<dyn SerializableComputation> {
        Box::new(bincode::deserialize::<T>(data).expect("Failed to deserialize"))
    }
}

The problem arises when I try to override the methods of the SerializableComputation trait. For example:

struct MyComputation {}

impl Computation for MyComputation {
    ...
}

impl SerializableComputation for MyComputation {
    ....
}

Gives me:

conflicting implementations of trait `SerializableComputation` for type `MyComputation`

It seems that because I've provided a blanket implementation of SerializableComputation for all types implementing Computation, I can't write a custom implementation for specific structs like MyComputation.


My Goal

I want to provide a default implementation for SerializableComputation that works for most cases but also allows users to override these methods with custom implementations for their specific structs.

What I've Tried

  1. Using the current blanket implementation: This works, but it prevents me from overriding SerializableComputation methods in specific structs because of the conflict.
  2. Moving methods inside SerializableComputation directly: This approach requires adding bounds like Clone + Serialize to SerializableComputation, which leads to issues when trying to create trait objects (dyn Computation can't be used due to object safety concerns).

You might be able to get away with writing a default impl for SerializableComputation instead of a blanket impl. It requires a little extra work, as you discovered, namely:

  1. The supertrait cycle needs to be broken. I decided to remove the Computation supertrait from SerializableComputation.
  2. Without a blanket impl, you need to add impl SerializableComputation for DefaultComputation {} to each Computation that you want to also be serializable. It's just a one-liner that can be derived trivially with #[derive(SerializableComputation)]!
  3. Making the trait methods default means that they can't return Box<dyn Trait>. They can return Self instead.

Here is how I did it:

2 Likes

Well, your blanket implementation is more restricted than all types implementing Computation,[1] so that depends.

The overlap error in such a case often happens because negative reasoning in coherence is limited. For example, just because a foreign type doesn't implement Clone today doesn't mean it's not allowed to implement it tomorrow.[2] So coherence will assume the blanket implementation may apply to the foreign type, even if it doesn't today (due to unmet bounds on the implementation).

However, negative reasoning is allowed locally. So if MyComputation is a local type, you should only get an overlap error if MyComputation actually meets the bounds (i.e. there is an actual overlap today).[3] Now, for that use case, you're basically wishing for specialization, which has stalled out (won't be coming to stable any time soon). So any (stable) approach for overriding the implementation for types that meet the blanket bounds is going to be some sort of workaround.


Here's one such workaround: add a trait to specifically opt in to the default implementation.

pub trait SerializableOptIn
where
  Self: 'static + Computation + Send + Sync + Serialize + for<'de> Deserialize<'de> + Clone
{}

impl<T: SerializableOptIn> SerializableComputation for T {
    // ...
}

Now downstream can choose to implement SerializableOptIn (if they've implemented the required supertrait bounds) or SerializableComputation for their own types. They'll only get an overlap error if they try to implement both, thanks to the local negative reasoning discussed above.

It might not be very idiomatic, and they can't choose a subset of methods to implement.

Another (orthogonal) tact is to supply free functions for the common approach, to make implementing the "default behavior" easier for downstream. The bounds required on the free functions can also be relaxed to what the individual "default method" needs.


  1. and if it wasn't you could just use default method bodies ↩︎

  2. without a new major release ↩︎

  3. Example. ↩︎

2 Likes

Thank you for your response! The solution seems to work but again, it gives many errors once I am trying to do something like this:

let computations: Vec<Computation> = vec![];
 Compiling playground v0.0.1 (/playground)
error[E0038]: the trait `Computation` cannot be made into an object
   --> src/main.rs:59:23
    |
59  |     let computations: Vec<Computation> = vec![];
    |                       ^^^^^^^^^^^^^^^^ `Computation` cannot be made into an object
    |
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
   --> src/main.rs:85:24
    |
85  | pub trait Computation: SerializableComputation {
    |           -----------  ^^^^^^^^^^^^^^^^^^^^^^^ ...because it requires `Self: Sized`
    |           |
    |           this trait cannot be made into an object...
...
91  |     Self: Serialize + for<'de> Deserialize<'de> + Clone
    |                       ^^^^^^^^^^^^^^^^^^^^^^^^^   ^^^^^ ...because it requires `Self: Sized`
    |                       |
    |                       ...because it requires `Self: Sized`
    |
   ::: /playground/.cargo/registry/src/index.crates.io-6f17d22bba15001f/serde-1.0.214/src/ser/mod.rs:256:8

It's hard to grasp because the code I initially posted works. I can have a collection of computations and call methods like run or serialize_computation on them without any issues. However, moving the code directly into SerializableComputation breaks this functionality."

Thanks! Using SerializableOptIn solves the issue by providing a way to override the default methods. However, as you mentioned, it has its downsides—I can’t override just a subset of methods. Additionally, each computation now needs to explicitly implement either SerializableOptIn or SerializableComputation.

Yes, Vec<Computation> is invalid because Computation is a trait. You need a trait object (the erased type must be behind a pointer) like Vec<Box<dyn Computation>>. And moving the SerializableComputation methods to default impl makes trait objects impossible with this trait: Deserialize requires Sized and there is no way to implement the SerializableComputation trait without it. (You can drop the Clone constraint, but it's not enough.)

Since you need to type erase (using trait objects), that rules out default impls for SerializableComputation.

My experience with trait objects has been pretty painful beyond trivial uses. dyn-compatibility/object safety has very bad ergonomics.

But, if you want dyn Computation instead of dyn SerializableComputation, maybe there is still a way out? The supertrait on Computation: SerializableComputation is what makes Computation non-dyn-compatible when SerializableComputation has a default impl with Deserialize. If you are able to remove the supertrait, then it can work... But it has the problem that you can no longer use the SerializableComputation methods after type erasure.

On the other hand, macros can do a lot of the heavy lifting by producing pretty much anything into the parser's token stream. So, let's say you write some helper macros to implement the trait and its methods:

Now you can freely mix and match "default methods" with custom method overrides. Using the macros is sort of a "syntactic salt" that gets sprinkled around with this approach. But it preserves the ask for default impls and type erasure.

1 Like

Thank you both! I really appreciate your responses. I aimed to keep the client code as simple as possible, so I decided to use a macro with derive. Now, users of the library can override the serialization methods with their own implementations if needed, or simply use the default ones by deriving SerializableComputation.

I still face the challenge of not being able to override parts of the implementation. However, I can simplify this by providing some free functions, as you also suggested.