Implement Complex Numbers in Rust

·

Rust has support for generics but sometimes, can be really hard to get your head around. That is because of its strong, edge cutting type system.

That good news is that Traits are here to help, in particular, when it comes to “shape” a generic type via trait bounds.

Implementing complex numbers helped me get a grasp on how to use generic types and traits in Rust.

If u are not comfortable with complex numbers, take a quick look to this quick introduction

Generics

So what do I mean by “Implementing Complex Numbers in Rust”?

I need a data structure which has multiplication (cross product), dot product with another complex number, addition, comparison, has its complex conjugate and has a standard way to be displayed.

A complex number hold two numbers, so let’s start with:

struct Complex {
    Real: f64,
    Imaginary: f64,
}

Here is already a great opportunity to add generics. (Real, Imaginary) can be any type number: f32, f64 or even i128 (quite dangerous).

The great news here is that those types are Copy types and Complex<T>, being composed of Copy types, can also a Copy type. That means overall less hassle with the Borrow Checker.

The crate num_traits has a handy trait Num that unifies all types of numbers.

use num_traits::Num as Number;

#[derive(PartialEq, PartialOrd, Eq, Copy, Clone, Debug)]
struct Complex<T: Number> {
    Real: T,
    Imaginaire: T,
}

impl<T: Copy + Number> Complex<T> {
     pub fn new(imaginary: T, real: T) -> Complex<T> {
        Complex {
            Real: real,
            Imaginaire: imaginary,
        }
    }
}

Overloading operations

Rust supports operator overloading which is nice for better reading.

Instead of having to invoke add and mul functions for all complex, using overloaded operations for more concise (less verbose in Rust loll)

Complex<T> * Complex<T> => Complex<T>  // (cross product not dot product)
Complex<T> + Complex<T> => Complex<T>

Overloading is done by implementing a trait: the trait Add<Complex<T>>.

For +, Complex<T> has to implement the Add<Complex<T>> trait with

impl<T: Copy + Number + Neg<Output = T>> Add<Complex<T>> for Complex<T> {
    type Output = Complex<T>;

    fn add(self, another: Complex<T>) -> Complex<T> {
        Complex {
            Imaginaire: another.Imaginaire + self.Imaginaire,
            Real: another.Real + self.Real,
        }
    }
}

And for *, Complex<T> has to implement the Mul<Complex<T>> trait with

impl<T: Copy + Number + Neg<Output = T>> Mul<Complex<T>> for Complex<T> {
    type Output = Complex<T>;

    fn mul(self, another: Complex<T>) -> Complex<T> {
        let real = (self.Real * another.Real) - (self.Imaginaire * another.Imaginaire);
        let im = (self.Real * another.Imaginaire) + (self.Imaginaire * another.Real);

        Complex {
            Real: real,
            Imaginaire: im,
        }
    }
}

Dot Product:

pub fn dot_product(&self, another: Complex<T>) -> T {
    (self.Real * another.Imaginaire) - (self.Imaginaire * another.Real)
}

for conjugate:

pub fn conjugate(&self) -> Complex<T> {
    Complex {
        Imaginaire: -self.Imaginaire,
        Real: self.Real,
    }
}

Traits for the win

While implementing the Display Trait for Complex<T>, I fell on the need of zero. I needed to know if the sign of the Imaginary part of my current number or if it was nil.

By chance, numbers in Rust implement the Zero Trait. YOUPPPIII That Trait gives access to the zero of a generic type.

impl<T: Copy + Number + Zero + PartialOrd + Display> Display for Complex<T> {
    fn fmt(&self, f: &mut Formatter) -> Result {
        match self {
            &Complex {
                Real: real,
                Imaginaire: im,
            } if real.is_zero() && !im.is_zero() => write!(f, "{}i", im),
            &Complex {
                Real: real,
                Imaginaire: im,
            } if !real.is_zero() && im.is_zero() => write!(f, "{}", real),
            &Complex {
                Real: real,
                Imaginaire: im,
            } if im < T::zero() => write!(f, "{}{}i", real, im),
            &Complex {
                Real: real,
                Imaginaire: im,
            } => write!(f, "{}+{}i", real, im),
        }
    }
}

impl<T: Clone + Copy + Neg<Output = T> + Number + Zero> Zero for Complex<T> {
    fn zero() -> Complex<T> {
        Complex::new(T::zero(), T::zero())
    }

    fn is_zero(&self) -> bool {
        self.Real.is_zero() && self.Imaginaire.is_zero()
    }
}

Example

let i = complex::Complex::<f64>::new(3.0, 3.0);
let j = complex::Complex::<f64>::new(3.0, 3.0);

println!("ADD: {}", j + i);     //  ADD: 6+6i
println!("MULT: {}", j * i);    //  MULT: 0+18i
println!("DOT Product: ({}).({}) = {}", i, j, i.dot_product(j));  //  DOT Product: (3+3i).(3+3i) = 0
println!("Conjugate: {}", i.conjugate());  //  Conjugate: 3-3i

All the code is available, here. However this library is way more complete, even if it share the same thought process.

blog

© All rights reserved. Powered by Astro with ♥.