Testing equality with a trait object

I have a trait object Schema and in my tests I would like to use assert_eq! to make sure functions like get (as represented below) and other schema generation functions are working correctly. I can't do trait Schema: PartialEq<Self> because of the Self part. I can't seem to impl PartialEq<Schema> for Schema and override with #[derive(PartialEq)] on implementing structs. And I can't downcast from a trait object, as in (SchemaBoolean::new() as &Schema) as &SchemaBoolean.

How can I get the following example to work?

trait Schema {
  fn get(&self, mut pointer: Pointer) -> Option<&Schema>;
  fn validate_value(&self, value: &Value) -> Result<(), Error>;
}

struct SchemaNull {…}
impl Schema for SchemaNull {…}

struct SchemaBoolean {…}
impl Schema for SchemaBoolean {…}

struct SchemaNumber {…}
impl Schema for SchemaNumber {…}

#[test]
fn assert_eq_trait_object() {
  assert_eq!(&SchemaBoolean::new() as &Schema, &SchemaBoolean::new() as &Schema);
}

#[test]
#[should_panic]
fn assert_eq_trait_object_fail() {
  assert_eq!(&SchemaNumber::new() as &Schema, &SchemaNull::new() as &Schema);
}
trait Schema where Self: PartialEq<Self> {
}

#[derive(PartialEq)]
struct S;


impl Schema for S {}

fn main() {}

This does not work as the compiler complains that Schema can not be made into an object.

This is due to the fact that trait Schema where Self: PartialEq<Self> uses Self which makes the trait not able to become a trait object.

@matklad yeah @svmnotn's right it doesn't work. But I didn't know that syntax was a thing.

No, you can't do this. Here's one reason why: derive(ParitalEq) produces a PartialEq<Self> impl, but you need each type to implement PartialEq<Schema>. This is very different, because you need to be able to do an equality check between all the different types, as in: SchemaNull == SchemaBoolean. The derived impl would reject that as badly typed, not return false.

You can do something like this, delegating equality to a method defined on Schema. Note that because the rhs is an opaque Schema object, you can only define equality in terms of the methods exposed by all types which implement Schema.

Or you can use Any:

With impl specialization, you could have default implementations for those methods as well.

Yep, I was so eager to show that you can use arbitrary type bounds in a where clause that I 've completely missed the part about trait objects, thanks for the correction!

This is similar to, but not exactly the same as, implementing PartialEq directly on the trait object:

trait Schema {
    fn value(&self) -> i32;
}

impl PartialEq<Schema> for Schema {
    fn eq(&self, other: &Schema) -> bool {
        self.value() == other.value()
    }
}

fn main() {}

If you want equality to work like types of objects are equal and contents of objects are equal, maybe you want to use an enum instead of a trait object?

Decided to try this using Debug since the equality checking doesn't have to be production grade (only for tests). Do you see any caveats with this approach? See the following snippet of code we are using.

trait Schema: Debug {…}

// Only compiled when testing because this is not a production quality solution.
#[cfg(test)]
impl PartialEq<Schema> for Schema {
  fn eq(&self, other: &Self) -> bool {
    format!("{:?}", self) == format!("{:?}", other)
  }
}

This looks like a nice approach, if the Debug-PartialEq solution doesn't work I will definitely try this next.


However, now there is another problem which maybe deserves its own thread. When trying to test equality on two reference trait objects I get two compiler errors which don't exist in the stable compiler error index or nightly compiler error index (E0495 and E0478). Here's what I'm trying to do:

fn assert_schema_get_eq(a: &Schema, pointer: Pointer, b: &Schema) {
  // The `get` method returns an `Option<&Schema>`
  assert!(a.get(pointer).unwrap().eq(b));
}

I get the following error message:

src/schema/schema.rs:482:15: 482:18 error: cannot infer an appropriate lifetime for autoref due to conflicting requirements [E0495]
src/schema/schema.rs:482     assert!(a.get(pointer).unwrap().eq(b));
                                       ^~~
src/schema/schema.rs:482:5: 482:44 note: in this expansion of assert! (defined in <std macros>)
src/schema/schema.rs:482:13: 482:14 note: first, the lifetime cannot outlive the expression at 482:12...
src/schema/schema.rs:482     assert!(a.get(pointer).unwrap().eq(b));
                                     ^
src/schema/schema.rs:482:5: 482:44 note: in this expansion of assert! (defined in <std macros>)
src/schema/schema.rs:482:13: 482:14 note: ...so that auto-reference is valid at the time of borrow
src/schema/schema.rs:482     assert!(a.get(pointer).unwrap().eq(b));
                                     ^
src/schema/schema.rs:482:5: 482:44 note: in this expansion of assert! (defined in <std macros>)
src/schema/schema.rs:482:13: 482:27 note: but, the lifetime must be valid for the method call at 482:12...
src/schema/schema.rs:482     assert!(a.get(pointer).unwrap().eq(b));
                                     ^~~~~~~~~~~~~~
src/schema/schema.rs:482:5: 482:44 note: in this expansion of assert! (defined in <std macros>)
src/schema/schema.rs:482:13: 482:14 note: ...so that method receiver is valid for the method call
src/schema/schema.rs:482     assert!(a.get(pointer).unwrap().eq(b));
                                     ^
src/schema/schema.rs:482:5: 482:44 note: in this expansion of assert! (defined in <std macros>)
src/schema/schema.rs:482:40: 482:41 error: lifetime bound not satisfied [E0478]
src/schema/schema.rs:482     assert!(a.get(pointer).unwrap().eq(b));
                                                                ^
src/schema/schema.rs:482:5: 482:44 note: in this expansion of assert! (defined in <std macros>)
src/schema/schema.rs:481:69: 483:4 note: lifetime parameter instantiated with the anonymous lifetime #2 defined on the block at 481:68
src/schema/schema.rs:481   fn assert_schema_get_eq(a: &Schema, pointer: Pointer, b: &Schema) {
src/schema/schema.rs:482     assert!(a.get(pointer).unwrap().eq(b));
src/schema/schema.rs:483   }
note: but lifetime parameter must outlive the static lifetime
error: aborting due to 2 previous errors

If you think I should make this its own post, let me know.

If you're using the PartialEq impl from above, you'll have a problem, because there's an implicit 'static bound on each appearance of Schema as a type. Try the impl and method signatures from the example I posted.

1 Like

You should change this to

impl<'a, 'b> PartialEq<Schema + 'b> for Schema + 'a

to make the impl clause more general.

1 Like

Yeah, that worked, thanks guys! Although I'm not 100% sure I understand how it works. Is it because in the way I had it Rust assumed the same lifetime for both schemas?

Without annotation, trait objects are assumed to be 'static objects, because this is the most conservative assumption. so impl Foo for Schema is the same as impl Foo for Schema + 'static

1 Like

It's a bit more subtle than that. Every time you have &'_1 Schema, it's equivalent to &'_1 (Schema + '_1). Same for mutable references. However, when Schema is used as a type parameter, such as as the Self type or the RHS parameter type in the PartialEq impl, it's implicitly Schema + 'static. This is where the incompatibility is coming from. PartialEq::eq has the signature (&Self, &RHS) -> bool, which turns into (&'_1 (Schema+'static), &'_2 (Schema+'static)) -> bool through substitution, whereas assert_schema_get_eq has the signature (&'_1 (Schema+'_1), Pointer, &'_2 (Schema + '_2)), so you get an incompatibility.

What's somewhat confusing is why <Schema+'static as PartialEq<Schema+'static>>::eq is allowed to be implemented by a method with the signature (&'_1 (Schema+'static), &'_2 (Schema+'_2)), which is the case in the erroneous impl which was causing you problems. It shouldn't cause any soundness issues, but it's a weird exception to the rule requiring the impl method's signature to exactly match the corresponding trait method's signature, especially since this only works when the lifetime is supposed to be 'static but isn't. Sure, in this case the impl signature is strictly more permissive, so it's fine, but the compiler isn't as forgiving in other situations where that's the case.