Trait objects, double dispatch, binary operation


#1

I’d like to define a binary operation on a vector of trait objects where the operation is not commutative and I came up with code like this.
It feels weird though because I have the type names like ‘S1’ in the function names.
The second problem is that I can’t make S1 or S2 generic, because the functions in the Combine trait would need to be generic which would prevent me from using trait objects.

What would be the Rust way of doing this? Thank you!

trait Combine {
	fn combine(&self, rhs: &Combine);
	fn combine_S1(&self, lhs: &S1);
	fn combine_S2(&self, lhs: &S2);
}

// Problem: can't make S1 generic, it would interfere with trait objects.
struct S1 {
	x: i32,
}

struct S2 {
	y: f64,
}

impl Combine for S1 {
	fn combine(&self, rhs: &Combine) {
		rhs.combine_S1(&self);
	}
	fn combine_S1(&self, lhs: &S1) {
	}
	fn combine_S2(&self, lhs: &S2) {
	}
}

impl Combine for S2 {
	fn combine(&self, rhs: &Combine) {
		rhs.combine_S2(&self);
	}
	fn combine_S1(&self, lhs: &S1) {
		// This is the only tested case in this example:
		println!("Combine: S1 x S2");
	}
	fn combine_S2(&self, lhs: &S2) {
	}
}

#[test]
fn test() {
	let mut v: Vec<Box<Combine>> = vec![];
	v.push(Box::new( S1 { x: 12 } ));
	v.push(Box::new( S2 { y: 4.5 } ));
	v[0].combine(&*v[1]);
}

#2

From what I understand, you want to be able to make a heterogeneous collection of types that implement a non-commutative operation. That means that for any two types in the collection you must have implementations of Combine<Type1, Type2> and Combine<Type2, Type1>. So if S1 and S2 implement Combine for each other then the Combine implementation for another struct S3 requires that Combine<S3, S1> + Combine<S3, S2> + Combine<S1, S3> + Combine<S2, S3> must be implemented. I don’t see an easy way to express that currently. I’m not sure it’s possible.

This might be one of the reasons why you end up with combine_S1() and combine_S2() in your trait. You’re trying to express the bound I just described.

A more typical op trait for Rust would be something like:

trait Combine<Rhs: ?Sized = Self> {
    fn combine(&self, other: &Rhs);
}

Since your operation is not commutative you might even want it to be:

trait Combine<Rhs: Combine<Self> + ?Sized = Self> {
    fn combine(&self, other: &Rhs);
}

Then the closest you might get to do what you want is using something like Box<Any> and downcasting. But I don’t recommend it. It’s usually a bad practice, akin to forgoing the type system and implementing dynamic typing in a statically typed language.

I’m sorry if that doesn’t help :confused:. Maybe someone has a better understanding + solution than me here.


#3

Thank you very much for your suggestions!

I was under the impression that I can not make the Combine trait generic because if it would be generic then I could not make trait objects of type &Combine to be used in my Vec<Box<Combine>>.

Yes, I would prefer to not conditionally downcast. I had a quick look at Any but that seems not elegant like you said. That’s why I tried this double-dispatch approach like I know it from c++, even though it looks weird in Rust because I can not overload the function.

The other issue, that my S1 can not be generic, is hopefully solved now by another non-generic trait T1 which exposes the non-generic subset of S1 functionality that I need.

I’ve updated it here, also extended to 3 different structs which can be combined:
https://play.rust-lang.org/?gist=d2ad67f8dfeefdbfc2be394edf6a1bf7
and I’d love to hear your thoughts.


#4

Yes you’re right, if Combine is generic then you have to specify a type (e.g. Box<Combine<Foo>>).

Regarding your code, I won’t give a definite opinion since it’s rather abstract and I don’t really know what we’re talking about. It works but it seem like a clumsy workaround. If you have to add a method to a trait for each new type that wants to implement it, it doesn’t sound like you’re really abstracting a behavior over types.

Now again I don’t know your situation, so it’s possible there’s no other good solution. But I’d suggest looking at other changes. Do you really need a heterogeneous collection? Or maybe a enum type could help ?

enum S {
    S1 { x: i32 },
    S2 { y: f64 },
}

or an enum of wrappers:

struct S1 { x: i32 }
struct S2 { y: f64 }

enum S {
    S1(S1),
    S2(S2),
}

#5

Very true, it does not abstract much anyway…
I’ll look at the enum approach again. Thank you!


#6

I think the enum wrapper does not cut it for me because I’d like (at least I think that I’d like) the S[n] to carry some “payload” of matrices and tensors of varying dimensions and using different layout in the background for the sparse matrices, so I wanted the S[n] to be generic.
This forces the enum S to be generic as far as I see.
But then I can not have a vector of those enum S if they carry a generic type parameter.
At least I couldn’t convince the compiler so far.
I’ll try to push the double dispatch approach and see how far it takes me…


#7

All right!