Crate symm_impl[][src]

Attribute macro that automatically implements a symmetric trait

Motivation

There is a class of binary operators known as symmetric operators. Formally, let F: D x D -> V be a binary operator, F is symmetric if F(a, b) = F(b, a) for any (a, b).

Such pattern arises naturally in computational geometry. For example, is_intersected(a: Shape1<Transform>, b: Shape2<Transform>) -> bool decides whether two transformed shapes intersects. Or distance(a: Shape1<Transform>, b: Shape2<Transform>) -> f32 computes the distance between two transformed shapes. Both functions could be seen as symmetric binary operators. More importantly, the types of the arguments can be heterogeneous. We can have point.distance(circle) for example.

It is very tempting to represent an operator with a trait:

trait Distance<Other> {
    fn distance(&self, other: &Other) -> f64;
}

And given different shapes:

struct Point2D {
    x: f64,
    y: f64,
}

struct Disk {
    center: Point2D,
    radius: f64
}

We can have

impl Distance<Point2D> for Point2D {
    fn distance(&self, other: &Point2D) -> f64 {
        let dx = self.x - other.x;
        let dy = self.y - other.y;
        (dx * dx + dy * dy).sqrt()
    }
}

impl Distance<Disk> for Point2D {
    fn distance(&self, other: &Disk) -> f64 {
        let p_diff = self.distance(&other.center);
        if p_diff.le(&other.radius) {
            0.0_f64
        } else {
            p_diff - other.radius
        }
    }
}

It is very helpful to also have impl Distance<Point2D> for Disk, but we cannot use generic implementation due to conflicting implementation.

// Conflicting implementation because this generic implementation makes
// Disk: Distance<Point2D>, which in turn implements
// Distance<Disk> for Point2D again.
impl<T, U> Distance<U> for T
where
    U: Distance<T>, {
    fn distance(&self, other: &U) -> f64 {
        other.distance(self)
    }
}

So one has to manually implement:

impl Distance<Point2D> for Disk {
    fn distance(&self, other: &Point2D) -> f64 {
        other.distance(self)
    }
}

This crates tries to address this problem by introducing an attribute macro to automatically implement the symmetric case.

Note

There are several constraints for a trait to be deemed symmetric:

  • The trait must be generic, with the first non-lifetime parameter being the type for the symmetry.

    e.g.

    trait SymmetricTrait<'a, Other, MoreType> {
        fn operator(&self, other: &Other) -> MoreType;
    }
    trait NotSymmetricTrait<'a, MoreType, Other> {
        fn operator(&self, other: &Other) -> MoreType;
    }
  • All the methods in the trait must take exactly 2 arguments, where the first argument is self and the other argument is of the type for the symmetry. The two arguments must have the same family in the sense that they should both or neither be reference or mutable.

    e.g.

    trait SymmetricTrait<Other> {
        fn operator_1(&self, other: &Other) -> SomeType;
        fn operator_2(self, other: Other) -> SomeType;
        fn operator_3(&mut self, other: &mut Other) -> SomeType;
    }
    trait NotSymmetricTrait<Other> {
        // reference mismatch
        fn operator_1(&self, other: Other) -> SomeType;
        // mutability mismatch
        fn operator_2(&self, other: &mut Other) -> SomeType;
        // incorrect arguments order
        fn operator_3(other: &mut Other, this: &mut Self) -> SomeType;
        // incorrect number of arguments
        fn operator_4(&self, other: &Other, more_other: &Other) -> SomeType;
    }

Associated types in a trait are allowed, and they will be transformed as:

trait TraitWithType<Other> {
    type SomeType;
}
impl TraitWithType<B> for A {
    type SomeType = i32;
}
// #[symmetric] will expands to
impl TraitWithType<A> for B {
    type SomeType = <A as TraitWithType<B>>::SomeType;
}

Example

use symm_impl::symmetric;

trait Distance<Other> {
    fn distance(&self, other: &Other) -> f64;
}
struct Point2D {
    x: f64,
    y: f64,
}
struct Disk {
    center: Point2D,
    radius: f64
}
impl Distance<Point2D> for Point2D {
    fn distance(&self, other: &Point2D) -> f64 {
        let dx = self.x - other.x;
        let dy = self.y - other.y;
        (dx * dx + dy * dy).sqrt()
    }
}
#[symmetric]
impl Distance<Disk> for Point2D {
    fn distance(&self, other: &Disk) -> f64 {
        let p_diff = self.distance(&other.center);
        if p_diff.le(&other.radius) {
            0.0_f64
        } else {
            p_diff - other.radius
        }
    }
}
/* Expands to
impl Distance<Point2D> for Disk {
    #[allow(unused_mut)]
    #[inline]
    fn distance(&self, other: &Point2D) -> f64 {
        <Point2D as Distance<Disk>>::distance(other, self)
    }
}
*/

let p = Point2D { x: 5.0, y: 4.0 };
let c = Disk {
    center: Point2D { x: 1.0, y: -2.0 },
    radius: 3.0,
};
assert_eq!(p.distance(&c), c.distance(&p));

Attribute Macros

symmetric

See module-level documentation