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 withAdd<T> for Complex<T>. - Associated type bounds: Deciding if
T::Outputmust beTor can be more general. - Trait bounds: Whether to require
CopyorClonefor the scalar case, and what is most idiomatic forcore. - 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
Outputtypes toT? This could be more generic based on theAddimplementation ofT, 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. TheAdd<T>andSub<T>need RHS to beTto avoid overlapping trait impls, butOutputdoesn't need to be limited.
folkertdev: That runs into a conflicting impl error. (it's important that
CompleximplementsCopyfor 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 withAdd/Subimpls like those inrustc_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.