unlock unlimited code runs for $79, limited time →
NOSSLETTER.techLIVE
back to OSSpedia
api-designtype-systemsrefactoringtesting-strategy

implement Add and Sub for Complex

Project: rust-lang/rust | Language: Rust | Concepts: api-design, type-systems, refactoring, testing-strategy | Difficulty: mid
Link: https://github.com/rust-lang/rust/pull/161227

The problem

The standard library's unstable Complex<T> type did not yet implement the standard arithmetic traits Add and Sub. This meant that users could not use the + and - operators with Complex numbers, nor could they add/subtract scalars to/from a complex number. This limited the ergonomics and usefulness of the type.

Why it's non-trivial

A naive fix might just implement Add and Sub for Complex<T> where T: Add/T: Sub, but there are subtle Rust type system issues:

  • Overlapping trait impls: Implementing Add<Complex<U>> for Complex<T> can conflict with Add<T> for Complex<T>.
  • Associated type bounds: Deciding if T::Output must be T or can be more general.
  • Trait bounds: Whether to require Copy or Clone for the scalar case, and what is most idiomatic for core.
  • Test coverage: Ensuring all relevant cases (scalars, different numeric types, wrapping types, floats) are covered.

The approach

1. Add trait imports and derive Eq for Complex

+use crate::ops::{Add, Sub};

This brings the relevant operator traits into scope.

-#[derive(Clone, Copy, Debug, PartialEq)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]

Deriving Eq allows for equality comparisons when T: Eq, which is useful for integer complex numbers.

2. Implement Add and Sub for Complex

Add two complex numbers:

#[unstable(feature = "complex_numbers", issue = "154023")]
impl<T: Add> Add<Self> for Complex<T> {
    type Output = Complex<T::Output>;

    fn add(self, rhs: Self) -> Self::Output {
        Complex::new(self.re + rhs.re, self.im + rhs.im)
    }
}

This enables Complex<T> + Complex<T> for any T: Add, producing a Complex<T::Output>. No unnecessary bounds on T::Output.

Add a scalar to a complex number:

#[unstable(feature = "complex_numbers", issue = "154023")]
impl<T: Add<Output = T>> Add<T> for Complex<T> {
    type Output = Complex<T::Output>;

    fn add(self, rhs: T) -> Self::Output {
        Complex::new(self.re + rhs, self.im)
    }
}

Allows Complex<T> + T, only modifying the real part. The bound T: Add<Output = T> ensures the result type is still T.

Subtraction mirrors addition:

#[unstable(feature = "complex_numbers", issue = "154023")]
impl<T: Sub> Sub<Self> for Complex<T> {
    type Output = Complex<T::Output>;

    fn sub(self, rhs: Self) -> Self::Output {
        Complex::new(self.re - rhs.re, self.im - rhs.im)
    }
}
#[unstable(feature = "complex_numbers", issue = "154023")]
impl<T: Sub<Output = T>> Sub<T> for Complex<T> {
    type Output = Complex<T::Output>;

    fn sub(self, rhs: T) -> Self::Output {
        Complex::new(self.re - rhs, self.im)
    }
}

3. Add feature flag for tests

+#![feature(complex_numbers)]

Enables the unstable Complex type for tests.

4. Add comprehensive tests

A new test file covers:

  • Addition and subtraction of two complex numbers
  • Addition and subtraction of a scalar
  • Wrapping integer types
  • Floating point types

Example test:

#[test]
fn complex_addition() {
    let a = Complex::new(1, 2);
    let b = Complex::new(3, 4);
    assert_eq!(a + b, Complex::new(a.re + b.re, a.im + b.im));
    assert_eq!(a + b, b + a);
    assert_eq!(a + 8, Complex::new(a.re + 8, a.im));
    // ... more cases ...
}

From the review thread

beetrees: Any reason to limit the RHS and Output types to T? This could be more generic based on the Add implementation of T, e.g.:

impl<T: Add<U>, U> Add<Complex<U>> for Complex<T> {
    type Output = Complex<T::Output>;

    fn add(self, rhs: Complex<U>) -> Self::Output {
        Complex::new(self.re + rhs.re, self.im + rhs.im)
    }
}

Similar thinking applies to the Sub<Self> implementation. The Add<T> and Sub<T> need RHS to be T to avoid overlapping trait impls, but Output doesn't need to be limited.

folkertdev: That runs into a conflicting impl error. (it's important that Complex implements Copy for that error to show up)

https://godbolt.org/z/ExhY3oacd

beetrees: That makes sense. Just removing the <Output = T> shouldn't cause any issues since it's just an associated type (compiler explorer), so doing that seems worthwhile (e.g. this would allow usage with Add/Sub impls like those in rustc_apfloat)

folkertdev: Sure, that seems fine. Fixed

Takeaways

  • Avoid overly restrictive trait bounds: Only constrain trait bounds as much as necessary; unnecessary bounds can limit generic usability.
  • Be careful with overlapping trait impls: Rust's coherence rules can cause conflicts if you try to implement traits for types in ways that overlap; test for these conflicts.
  • Comprehensive tests should cover all trait impls and edge cases: When adding core trait impls, ensure tests cover all relevant input types and scenarios, including edge cases like wrapping arithmetic and floats.