2
\$\begingroup\$

I am learning object oriented programming in Rust. I created my own struct for complex numbers. Could you please review and answer my questions:

  1. Is it possible to implement C++-like default constructor? I want to replace let c0 = my_math::Complex::default(); with let c0: Complex;.

  2. Why should I implement PartialEq instead of Eq?

  3. Why should I implement type Output = Complex;? I already have -> Complex.

     mod my_math {
        use std::fmt;
        use std::ops;
        use std::cmp::PartialEq;
    
        pub struct Complex {
            // Complex numbers (real, image * i)
            pub real: i32,
            pub imaginary: i32 
        }
    
        impl Complex {
            pub fn new(real: i32, imaginary: i32) -> Complex {
                Complex {real: real, imaginary: imaginary}
            }
        }
    
        impl Default for Complex {
            fn default() -> Self {
                Complex { real: 0, imaginary: 0}
            }
        }
    
        impl fmt::Display for Complex {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                write!(f, "({}, {}i)", self.real, self.imaginary)
            }
        }
    
        impl ops::Add<Complex> for Complex {
            type Output = Complex;
            fn add(self, rhs: Complex) -> Complex {
                Complex {real: rhs.real + self.real, imaginary: rhs.imaginary + self.imaginary}
            }
        }
    
        impl ops::Sub<Complex> for Complex {
            type Output = Complex;
            fn sub(self, rhs: Complex) -> Complex {
                Complex {real: self.real - rhs.real, imaginary: self.imaginary - rhs.imaginary}
            }
        }
    
        impl ops::Mul<Complex> for Complex {
            type Output = Complex;
            fn mul(self, rhs: Complex) -> Self::Output {
                Complex {
                    real: self.real * rhs.real - self.imaginary * rhs.imaginary, 
                    imaginary: self.real * rhs.imaginary + self.imaginary * rhs.real
                }
            }
        }
    
        impl PartialEq for Complex {
            fn eq(&self, other: &Self) -> bool {
                self.real == other.real && self.imaginary == other.imaginary
            }
        }
    
        
    }
    
    
    fn main() {
        let c0 = my_math::Complex::default();
        assert!(c0.real == 0);
        assert!(c0.imaginary == 0);
    
        let c1 = my_math::Complex{real: 1, imaginary: 10};
        let c2 = my_math::Complex::new(2, 20);
        
        let c3 = c1 + c2;
        let c3_expected = my_math::Complex::new(3, 30);
        assert!(c3 == c3_expected);
        
        println!("{}", c3);
    }    
\$\endgroup\$
3
  • 1
    \$\begingroup\$ We can review the code, but the specific questions aren't suitable for Code Review. We can't advise on changing the functionality of the code, nor explain why you wrote your code the way you did. \$\endgroup\$ Commented May 9, 2023 at 13:19
  • \$\begingroup\$ You may want to ask these questions on Stack Overflow. \$\endgroup\$ Commented May 9, 2023 at 13:43
  • \$\begingroup\$ @TobySpeight my aim is to learn the best practices of rust OOP \$\endgroup\$
    – mascai
    Commented May 9, 2023 at 20:21

2 Answers 2

5
\$\begingroup\$

Derive PartialEq (and possibly Eq too) and Default instead of manually implementing them

#[derive(PartialEq, Default)]
pub struct Complex {
     // Complex numbers (real, image * i)
     pub real: i32,
     pub imaginary: i32 
}

This simplifies the code.

Remove use std::cmp::PartialEq;

It's redundant - PartialEq is in the prelude.

Always #[derive(Debug)] for your types

This lets them be printed in a programmer-convienent format.

Implement Clone and Copy for Complex

#[derive(Clone, Copy)]
pub struct Complex {
     // Complex numbers (real, image * i)
     pub real: i32,
     pub imaginary: i32 
}

This lets Complex be copied when needed, and as a bonus, implementing Copy permits some optimizations with standard library types.

Use assert_eq!() when appropriate

Replace assert!(x == y) with assert_eq!(x, y). This prints x and y in case the assert fails. This requires them to implement Debug.

Implement Eq too

I don't know why you decided you should not implement it; you should implement it wherever you can.

Format your code with rustfmt

This makes your code follow the official Rust style.

Use derive_more

This is not necessary (I for one would not do that), but if you're fine with introducing a dependency, you can avoid the manual implementation of the ops and even new with derive_more:

#[derive(
    Debug,
    Clone,
    Copy,
    PartialEq,
    Eq,
    Default,
    derive_more::Constructor,
    derive_more::Add,
    derive_more::Sub,
)]
pub struct Complex {
    // Complex numbers (real, image * i)
    pub real: i32,
    pub imaginary: i32,
}

Change the comment inside Complex to be a doc comment

This will document the struct better.

/// Complex numbers (real, image * i)
pub struct Complex {
    pub real: i32,
    pub imaginary: i32,
}

Use struct field init shorthand

If you don't use derive_more for new(), instead of specifying field: field just say field:

pub fn new(real: i32, imaginary: i32) -> Complex {
    Complex { real, imaginary }
}

Don't specify RHS's type

It is defaulted to the same type as the implementing type, so no need to specify it.

impl ops::Mul for Complex { ... }

Final code

The code with all changes including derive_more:

mod my_math {
    use std::fmt;

    #[derive(
        Debug,
        Clone,
        Copy,
        PartialEq,
        Eq,
        Default,
        derive_more::Constructor,
        derive_more::Add,
        derive_more::Sub,
    )]
    /// Complex numbers (real, image * i)
    pub struct Complex {
        pub real: i32,
        pub imaginary: i32,
    }

    impl fmt::Display for Complex {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "({}, {}i)", self.real, self.imaginary)
        }
    }

    impl ops::Mul for Complex {
        type Output = Complex;
        fn mul(self, rhs: Complex) -> Self::Output {
            Complex {
                real: self.real * rhs.real - self.imaginary * rhs.imaginary,
                imaginary: self.real * rhs.imaginary + self.imaginary * rhs.real,
            }
        }
    }
}

fn main() {
    let c0 = my_math::Complex::default();
    assert_eq!(c0.real, 0);
    assert_eq!(c0.imaginary, 0);

    let c1 = my_math::Complex {
        real: 1,
        imaginary: 10,
    };
    let c2 = my_math::Complex::new(2, 20);

    let c3 = c1 + c2;
    let c3_expected = my_math::Complex::new(3, 30);
    assert_eq!(c3, c3_expected);

    println!("{}", c3);
}

The code with all changes not including derive_more:

mod my_math {
    use std::fmt;
    use std::ops;

    #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
    /// Complex numbers (real, image * i)
    pub struct Complex {
        pub real: i32,
        pub imaginary: i32,
    }

    impl Complex {
        pub fn new(real: i32, imaginary: i32) -> Complex {
            Complex { real, imaginary }
        }
    }

    impl fmt::Display for Complex {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "({}, {}i)", self.real, self.imaginary)
        }
    }

    impl ops::Add for Complex {
        type Output = Complex;
        fn add(self, rhs: Complex) -> Complex {
            Complex {
                real: rhs.real + self.real,
                imaginary: rhs.imaginary + self.imaginary,
            }
        }
    }

    impl ops::Sub for Complex {
        type Output = Complex;
        fn sub(self, rhs: Complex) -> Complex {
            Complex {
                real: self.real - rhs.real,
                imaginary: self.imaginary - rhs.imaginary,
            }
        }
    }

    impl ops::Mul for Complex {
        type Output = Complex;
        fn mul(self, rhs: Complex) -> Self::Output {
            Complex {
                real: self.real * rhs.real - self.imaginary * rhs.imaginary,
                imaginary: self.real * rhs.imaginary + self.imaginary * rhs.real,
            }
        }
    }
}

fn main() {
    let c0 = my_math::Complex::default();
    assert_eq!(c0.real, 0);
    assert_eq!(c0.imaginary, 0);

    let c1 = my_math::Complex {
        real: 1,
        imaginary: 10,
    };
    let c2 = my_math::Complex::new(2, 20);

    let c3 = c1 + c2;
    let c3_expected = my_math::Complex::new(3, 30);
    assert_eq!(c3, c3_expected);

    println!("{}", c3);
}
```
\$\endgroup\$
4
  • \$\begingroup\$ Why do you have only impl ops::Mul in derive_more iplementation? \$\endgroup\$
    – mascai
    Commented May 14, 2023 at 15:21
  • \$\begingroup\$ @mascai You mean why I don't have it? Because it's not multiplying the fields in order. \$\endgroup\$ Commented May 14, 2023 at 15:23
  • \$\begingroup\$ I'd also get rid of the redundant Complex type statements in the implementations and replace them with Self and Self::Output respectively. \$\endgroup\$ Commented May 24, 2023 at 0:55
  • \$\begingroup\$ @RichardNeumann This is subjective; some people prefer specifying the type name. \$\endgroup\$ Commented May 24, 2023 at 7:01
1
\$\begingroup\$

In addition to @Chayim's great answer, you can also make your struct Complex more generic and let the user decide what numeric types they want to use.

For this generic code, num_traits comes in very handy.

use num_traits::Num;

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct Complex<R, I>
where
    R: Num,
    I: Num,
{
    real: R,
    imaginary: I,
}

///
/// ```
/// use complex::Complex;
///
/// let num = Complex::new(-1, 10u8);
/// assert_eq!(num.real(), -1);
/// assert_eq!(num.imaginary(), 10u8);
///
/// let num = Complex::new(-1, 10u8);
/// assert_eq!(num.real(), -1);
/// assert_eq!(num.imaginary(), 10u8);
/// ```
impl<R, I> Complex<R, I>
where
    R: Clone + Num,
    I: Clone + Num,
{
    pub fn new(real: R, imaginary: I) -> Self {
        Self { real, imaginary }
    }

    pub fn real(&self) -> R {
        self.real.clone()
    }

    pub fn imaginary(&self) -> I {
        self.imaginary.clone()
    }

    /// # Examples
    /// ```
    /// use complex::Complex;
    ///
    /// let num = Complex::new(1, 10);
    /// assert!(num.as_real().is_err());
    ///
    /// let num = Complex::new(-12, 0);
    /// assert_eq!(num.as_real(), Ok(-12));
    /// ```
    pub fn as_real(&self) -> Result<R, &'static str> {
        if self.imaginary == I::zero() {
            Ok(self.real.clone())
        } else {
            Err("imaginary component is non-zero")
        }
    }
}

You may also want to consider to implement the common binary operations with real numbers:

/// # Examples
/// ```
/// use complex::Complex;
///
/// let lhs = Complex::new(1, 10);
/// let rhs = 5;
/// let sum = Complex::new(6, 10);
///
/// assert_eq!(lhs + rhs, sum);
/// ```
impl<R, I> Add<R> for Complex<R, I>
where
    R: Num,
    I: Num,
{
    type Output = Complex<R, I>;

    fn add(self, rhs: R) -> Self::Output {
        Self::Output {
            real: rhs + self.real,
            imaginary: self.imaginary,
        }
    }
}

/// # Examples
/// ```
/// use complex::Complex;
///
/// let lhs = Complex::new(4, -3);
/// let rhs = -3;
/// let product = Complex::new(-12, 9);
///
/// assert_eq!(lhs * rhs, product);
/// ```
impl<R, I, N> Mul<N> for Complex<R, I>
where
    R: From<N> + Num,
    I: From<N> + Num,
    N: Copy + Num,
{
    type Output = Complex<R, I>;

    fn mul(self, rhs: N) -> Self::Output {
        Self::Output {
            real: self.real * rhs.into(),
            imaginary: self.imaginary * rhs.into(),
        }
    }
}

/// # Examples
/// ```
/// use complex::Complex;
///
/// let lhs = Complex::new(4, -3);
/// let rhs = 12;
/// let difference = Complex::new(-8, -3);
///
/// assert_eq!(lhs - rhs, difference);
/// ```
impl<R, I> Sub<R> for Complex<R, I>
where
    R: Num,
    I: From<R> + Num,
{
    type Output = Complex<R, I>;

    fn sub(self, rhs: R) -> Self::Output {
        Self::Output {
            real: self.real - rhs,
            imaginary: self.imaginary,
        }
    }
}

Note that I use doctests to directly test the respective implementations for their correctness, instead of using a main function.

It also might be handy to be able to convert real numbers to complex numbers:

/// # Examples
/// ```
/// use complex::Complex;
///
/// let lhs: Complex<u32, i64> = 12.into();
/// let rhs = Complex::new(12u32, 0i64);
///
/// assert_eq!(lhs, rhs);
/// ```
impl<R, I> From<R> for Complex<R, I>
where
    R: Clone + Num,
    I: Clone + Num,
{
    fn from(real: R) -> Self {
        Self::new(real, I::zero())
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.