Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Operators that follow from the ones a type has

API reference on docs.rs: complete_ops.

Subtraction is not new information. If x + v makes sense, and v lies in an abelian group, then x - v makes sense too: -v lies in that same group, so x + (-v) is an addition the type already has. complete_ops writes that down, and claims nothing else.

use std::ops::{Add, Neg};
#[derive(Debug, PartialEq)]
#[complete_ops(Sub)]
struct Vector(f64);

#[overload_op]
impl Add<&Vector> for &Vector {
    type Output = Vector;

    fn add(self, rhs: &Vector) -> Vector {
        Vector(self.0 + rhs.0)
    }
}

#[overload_op]
impl Neg for &Vector {
    type Output = Vector;

    fn neg(self) -> Vector {
        Vector(-self.0)
    }
}

assert_eq!(Vector(5.0) - Vector(2.0), Vector(3.0));

let mut total = Vector(5.0);
total -= Vector(2.0);
assert_eq!(total, Vector(3.0));

The right operand stays generic

The completion says nothing about which type is subtracted, only that it is a group the attributed type can add:

impl<Rhs> Sub<Rhs> for Space
where
    Rhs: Neg<Output = Rhs>,
    Space: Add<Rhs>,
{
    type Output = <Space as Add<Rhs>>::Output;

    fn sub(self, rhs: Rhs) -> Self::Output {
        self + -rhs
    }
}

The completion is plain Rust, so it can be tried out directly. Edit the block below and run it, for example subtracting &Vector(2.0) to see a borrowed operand rejected:

use std::ops::{Add, Neg, Sub};

#[derive(Debug, Clone, Copy, PartialEq)]
struct Vector(f64);

impl Add for Vector {
    type Output = Vector;

    fn add(self, rhs: Vector) -> Vector {
        Vector(self.0 + rhs.0)
    }
}

impl Neg for Vector {
    type Output = Vector;

    fn neg(self) -> Vector {
        Vector(-self.0)
    }
}

// The impl `#[complete_ops(Sub)]` adds.
impl<Rhs> Sub<Rhs> for Vector
where
    Rhs: Neg<Output = Rhs>,
    Vector: Add<Rhs>,
{
    type Output = <Vector as Add<Rhs>>::Output;

    fn sub(self, rhs: Rhs) -> Self::Output {
        self + -rhs
    }
}

fn main() {
    println!("{:?}", Vector(5.0) - Vector(2.0));
}

Neg<Output = Rhs> is what makes the operand a group element rather than merely something negatable: an inverse stays in the group it came from. One impl therefore covers every group the type can add, rather than one impl per operand type, and the result type is whatever that addition already returns. Rhs and the lifetime of the borrowed operand are only default names: when the declaration already writes either, as a parameter, a bounded type, or its own name, the introduced one is numbered instead, as Rhs2. Nothing about the attributed type’s own structure is assumed, so a space acted on by a group completes exactly as a group acting on itself does:

use std::ops::{Add, Neg};
#[derive(Debug, PartialEq)]
struct Vector(f64);

#[derive(Debug, PartialEq)]
#[complete_ops(Sub)]
struct Point(f64);

#[overload_op]
impl Add<&Vector> for &Vector {
    type Output = Vector;

    fn add(self, rhs: &Vector) -> Vector {
        Vector(self.0 + rhs.0)
    }
}

#[overload_op]
impl Neg for &Vector {
    type Output = Vector;

    fn neg(self) -> Vector {
        Vector(-self.0)
    }
}

#[overload_op]
impl Add<&Vector> for &Point {
    type Output = Point;

    fn add(self, rhs: &Vector) -> Point {
        Point(self.0 + rhs.0)
    }
}

assert_eq!(Point(5.0) - Vector(2.0), Point(3.0));

Point has no addition of its own and no negation. It is only a space the group acts on, and translating it backwards is still x + (-v).

What it does not claim

The completion covers exactly the subtractions that are additions of an inverse, and the difference of two points of a space is not one of them. It is not merely a different operation: in general it does not exist. Take the circle acted on by the reals, t rotating a point by t. The action is transitive, but recovering a real number from two points of the circle needs a choice of branch of the logarithm, and no such choice follows from the action. So x - x' is never completed. Where a type does have one, it stays hand-written and coexists with the completion:

use std::ops::{Add, Neg, Sub};
#[derive(Debug, PartialEq)]
struct Vector(f64);

#[derive(Debug, PartialEq)]
#[complete_ops(Sub)]
struct Point(f64);
#[overload_op]
impl Add<&Vector> for &Vector {
    type Output = Vector;
    fn add(self, rhs: &Vector) -> Vector { Vector(self.0 + rhs.0) }
}
#[overload_op]
impl Neg for &Vector {
    type Output = Vector;
    fn neg(self) -> Vector { Vector(-self.0) }
}
#[overload_op]
impl Add<&Vector> for &Point {
    type Output = Point;
    fn add(self, rhs: &Vector) -> Point { Point(self.0 + rhs.0) }
}

impl Sub<&Point> for &Point {
    type Output = Vector;

    fn sub(self, rhs: &Point) -> Vector {
        Vector(self.0 - rhs.0)
    }
}

assert_eq!(&Point(5.0) - &Point(2.0), Vector(3.0));
assert_eq!(Point(5.0) - Vector(2.0), Point(3.0));

That coexistence rests on the space having no negation of its own: the two impls are disjoint because Point: Neg<Output = Point> cannot hold. A type that is its own group has a negation, so its completed Sub claims every right operand and no other Sub can be written for it. That is usually what you want — there is no second subtraction on a group — but it is a door that closes.

Completing the negation too

A type that multiplies by a scalar does not need a hand-written negation either, since -x is x scaled by -1. Naming the scalar completes Neg, which the subtraction then goes through:

use std::ops::{Add, Mul};
#[derive(Debug, PartialEq)]
#[complete_ops(Neg = -1.0f64, Sub)]
struct Scaled(f64);

#[overload_op]
impl Add<&Scaled> for &Scaled {
    type Output = Scaled;

    fn add(self, rhs: &Scaled) -> Scaled {
        Scaled(self.0 + rhs.0)
    }
}

#[overload_op]
impl Mul<&f64> for &Scaled {
    type Output = Scaled;

    fn mul(self, rhs: &f64) -> Scaled {
        Scaled(self.0 * rhs)
    }
}

assert_eq!(-Scaled(2.0), Scaled(-2.0));
assert_eq!(Scaled(5.0) - Scaled(2.0), Scaled(3.0));

The scalar is written as an expression, so its type cannot be named in the generated header and the completion states no bound for the multiplication it performs. On a concrete declaration, as above, that costs nothing. On a generic one the multiplication must already be available for every instantiation, which means carrying the bounds on the declaration itself:

// `Pair<T>` negates only where every `Pair<T>` can be scaled by the scalar.
#[complete_ops(Neg = -1.0f64)]
struct Pair<T: Copy + Mul<f64, Output = T>>(T, T);

Without them the expansion fails inside the generated neg, reported against the #[complete_ops(...)] attribute rather than against the declaration.

Suffix the literal, as -1.0f64 above, whenever the type has more than one scalar multiplication: the value is passed to Mul::mul as written, so its type is what selects the impl. The order inside the attribute does not matter, since a completed negation is always emitted ahead of the subtraction that uses it. Each operator may be named once; a repeated one is rejected rather than producing conflicting impls.

Borrowed operands

A borrowed right operand is not a group element: negating &Vector yields an owned Vector, so &Vector: Neg<Output = &Vector> does not hold and the completion does not apply. x - &v is therefore rejected:

error[E0271]: type mismatch resolving `<&Vector as Neg>::Output == &Vector`

Subtract the group element itself, as x - v.

The borrowed left operand fails for an unrelated reason. The completion does emit impl Sub<Rhs> for &Lhs, but reaching it needs Add<Rhs> for &Lhs — the ref-plus-owned combination overload_op deliberately does not generate — so that impl stays inert unless the addition is written by hand.