Implementing a Rational type in Rust


Following my tradition of writing a rational number class/struct/type in a new programming language, I will develop a very basic module for rationals in Rust in this brief post.

First off, let’s create a new project to store our rational number type in.

Project setup

We create the project using the canonical way – using Cargo. Since we’re interested in building a library, we don’t pass in the --bin flag.

In case you are not familiar with Cargo, all you need to know is that it is the de facto build and dependency management tool for Rust projects. In terms of dependency management in particular, we specify the dependencies inside the Cargo.toml file, and Cargo takes care of downloading the library (and its dependencies, and their dependencies, and so on), builds them, and caches the libraries for future use by other projects (usually inside $HOME/.cargo).

Timmys-MacBook-Pro:Rust z0ltan$ cargo new rationals
     Created library `rationals` project
Timmys-MacBook-Pro:Rust z0ltan$ cd rationals

The following files and directories must have been automatically created for you:

Timmys-MacBook-Pro:rationals z0ltan$ tree
.
├── Cargo.toml
└── src
    └── lib.rs

1 directory, 2 files

So far so good! Now, since we would need to test the functionality, let’s create a test folder, and create a tests.rs file in that folder.

Timmys-MacBook-Pro:rationals z0ltan$ mkdir tests
Timmys-MacBook-Pro:rationals z0ltan$ touch tests/tests.rs
Timmys-MacBook-Pro:rationals z0ltan$ tree
.
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    └── tests.rs

2 directories, 3 files

Excellent! Now let’s get on to the code.

The Code

Top

Since the code itself is not very large, I thought it best to present the code in its entirety first, and then go about explaining the pertinent portions that do need explaining.

Insert the following code into src/lib.rs.

use std::fmt;
use std::ops::{ Add, Sub, Mul, Div };
use std::cmp::{ PartialEq, Eq };


#[derive(Clone, Copy, Debug)]
pub struct Rational {
	x: i32,
	y: i32,
}

impl Rational {
	fn gcd(x: i32, y: i32) -> i32 {
		if y == 0 {
			x
		} else {
			Rational::gcd(y, x%y)
		}
	}
	
	pub fn new(x: i32, y: i32) -> Rational {
		assert!(y != 0);

		let g = Rational::gcd(i32::abs(x), i32::abs(y));

		Rational {
			x: x/g,
			y: y/g,
		}
	}
}


impl fmt::Display for Rational {
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
		if self.y == 1 {
			write!(f, "{}", self.x)
		} else {
			write!(f, "{}/{}", self.x, self.y)
		}
	}
}

// to allow equality checks
impl PartialEq for Rational {
	fn eq(&self, other: &Rational) -> bool {
		(self.x == other.x) && (self.y == other.y)
	}
}

impl Eq for Rational {}


// All the add traits
impl Add for Rational {
	type Output = Rational;

	fn add(self, other: Rational) -> Rational {
		Rational::new(self.x * other.y + self.y * other.x, self.y * other.y)
	}
}

impl Add<i32> for Rational {
	type Output = Rational;

	fn add(self, other: i32) -> Rational {
		Rational::add(self, Rational::new(other, 1))
	}
}

impl Add<Rational> for i32 {
	type Output = Rational;

	fn add(self, other: Rational) -> Rational {
		Rational::add(Rational::new(self, 1), other)
	}
}

// All the sub traits
impl Sub for Rational {
	type Output = Rational;

	fn sub(self, other: Rational) -> Rational {
		Rational::new(self.x * other.y - self.y * other.x, self.y * other.y)
	}
}

impl Sub<i32> for Rational {
	type Output = Rational;

	fn sub(self, other: i32) -> Rational {
		Rational::sub(self, Rational::new(other, 1))
	}

}

impl Sub<Rational> for i32 {
	type Output = Rational;

	fn sub(self, other: Rational) -> Rational {
		Rational::sub(Rational::new(self, 1), other)
	}
}

// All the mul traits
impl Mul for Rational {
	type Output = Rational;

	fn mul(self, other: Rational) -> Rational {
		Rational::new(self.x * other.x, self.y * other.y)
	}
}

impl Mul<i32> for Rational {
	type Output = Rational;

	fn mul(self, other: i32) -> Rational {
		Rational::mul(self, Rational::new(other, 1))
	}
}

impl Mul<Rational> for i32 {
	type Output = Rational;

	fn mul(self, other: Rational) -> Rational {
		Rational::mul(Rational::new(self, 1), other)
	}
}


// All the div traits
impl Div for Rational {
	type Output = Rational;

	fn div(self, other: Rational) -> Rational {
		Rational::new(self.x * other.y, self.y * other.x)
	}
}

impl Div<i32> for Rational {
	type Output = Rational;

	fn div(self, other: i32) -> Rational {
		Rational::div(self, Rational::new(other, 1))
	}
}

impl Div<Rational> for i32 {
	type Output = Rational;

	fn div(self, other: Rational) -> Rational {
		Rational::div(Rational::new(self, 1), other)
	}
}

Explanatory notes: Top

The code is, as usual, straightforward. We define a struct (Rust does not have the concept of classes, but structS cover the basic use-cases of a class) called Rational to define our new type.

We then “implement” various traits (loosely speaking, similar to Java’s interfaces, but much more powerful) that add on features for our new type. The #[derive(Clone, Copy, Debug)] bit is called an “attribute” (again, loosely speaking, similar to Java’s annotations). This tells the Rust compiler that we wish this new data type to be cloneable, copyable, and well, debuggable (which means that the compiler will figure out a default way of displaying a Rational object).

We first implement the main functionality for the Rational type itself (impl Rational {), and provide a default constructor called new. We also define a private (which is the default mode, unless override by the pub keyword) function, gcd, which is used to reduce a Rational number to its normalised form. For instance, 2/6 is represented as 1/3 internally.

We then implement the Display trait (impl fmt::Display for Rational {). This is simply to allow us to print the Rational number in a custom way.

Now comes an interesting bit – in order to allow our custom type to be comparable (say, using the == operator), we need to implement the Eq trait. In Rust, however, this trait does nothing other than to define implementing the PartialEq trait, which specifies partial equality. The details are not important for this example though, and implementing PartialEq works fine for this demo.

Finally, we need to fill out the actual logic of various operations on rational numbers: addition, subtraction, multiplication, and division are covered here. The problem though is this – there are three basic combinations that come within this context:

  • Operations between two Rational numbers
  • Operations between a Rational number and an integer, and
  • Operations between an integer and a Rational number

A further complication arises in Rust specifically due to the fact that Rust is very very particular about the type of each variable/value. For example, even though the following types all represent signed integers – i8, i16, i32, i64, they are not interchangeable. Couple this with the fact that we don’t really have a common type or trait like Integer to cover all these types (yet another irksome quirk of Rust), covering all possible combinations of Rational number interoperation with integers would be a combinatory explosion!

To keep things sane, I have restricted usage to the default integer type – signed32-bit integers (i32).

As such, we have three different categories (as discussed above) of traits being implemented in this sections to cover all the use-cases:

  • Operations between two Rational numbers: This is handled by the following trait implementations –
    impl Add for Rational { 

    ,

    impl Sub for Rational { 

    ,

    impl Mul for Rational { 

    , and

    impl Div for Rational { 
  • Operations between a Rational number and an i32: This scenario is handle by the following implS –
    impl Add<i32> for Rational { 

    ,

    impl Sub<i32> for Rational { 

    ,

    impl Mul<i32> for Rational { 

    , and

    impl Div<i32> for Rational { 

    . As you can see, the operator overloading traits are generic to a great extent.

  • Operations between an i32 and a Rational number: This final use-case is covered by the following implS –
    impl Add<Rational> for i32 { 

    ,

    impl Sub<Rational> for i32 { 

    ,

    impl Mul<Rational> for i32 { 

    , and

    impl Div<Rational> for i32 { 

    .

    The interesting thing to note here is that we can implement traits even for built-in types like i32‘s! However, of course, this extra functionality is restricted to our rationals crate and its clients. It would be a nightmare if these changes were visible throughout the codebase!

  • You might have already realised it by now, based on the above, that Rust supports operator overloading. Just like Kotlin, however, it severely restricts the operators that you can overload, and the mechanism for overloading an operator is by implementing the corresponding trait(s) that represents that functionality (Talk about nice consistency!). I feel that this strikes a nice balance between the approach taken by C++ (where you can basically overload whatever you want, wherever you want), and that taken by Java (no operator overloading at all!). For anyone shaking his head at the latter, I dare you to try using the java.math.BigInteger class in any non-trivial program without getting severe depression!

    Note that the standard operator overloading traits are defined in the std::ops module of the standard Rust library.

    So there you go, the actual logic involved in this example is so trivial that I am skipping it altogether. What’s more interesting is the way in which it can be implemented in Rust.

    Now, let’s give it a spin, and assure ourselves that all is well with our implementation!

    Demo

    Top

    The following test code goes inside tests/tests.rs. One of the beauties of Cargo is that it can pick up all tests in any arbitrary path inside the Cargo project.

    extern crate rationals;
    
    
    #[cfg(test)]
    mod rationals_tests {
    	use rationals::Rational;
    
    
    	// Rationals with Rationals
    
    	#[test]
    	pub fn add_two_rationals() {
    		let r1 = Rational::new(1, 3);
    		let r2 = Rational::new(2, 6);
    
    		assert_eq!(Rational::new(2, 3), r1+r2);	
    	}
    
    	#[test]
    	pub fn sub_two_rationals() {
    		let r1 = Rational::new(1, 3);
    		let r2 = Rational::new(2, 6);
    
    		assert_eq!(Rational::new(0, 1), r1-r2);
    	}
    
    	#[test]
    	pub fn mul_two_rationals() {
    		let r1 = Rational::new(1, 3);
    		let r2 = Rational::new(2, 6);
    
    		assert_eq!(Rational::new(1, 9), r1*r2);
    	}
    
    	#[test]
    	pub fn div_two_rationals() {
    		let r1 = Rational::new(1, 3);
    		let r2 = Rational::new(2, 6);
    
    		assert_eq!(Rational::new(1, 1), r1/r2);
    	}
    
    
    	// Rationals with i32
    
    	#[test]
    	pub fn add_i32_to_rational() {
    		let r1 = Rational::new(1, 3);
    		let num = 2i32;
    
    		assert_eq!(Rational::new(7, 3), r1+num);
    	}
    
    	#[test]
    	pub fn sub_i32_from_rational() {
    		let r1 = Rational::new(1, 3);
    		let num = 2i32;
    
    		assert_eq!(Rational::new(-5, 3), r1-num);
    	}
    
    	#[test]
    	pub fn mul_rational_with_i32() {
    		let r1 = Rational::new(1, 3);
    		let num = 2i32;
    
    		assert_eq!(Rational::new(2, 3), r1*num);
    	}
    
    	#[test]
    	pub fn div_rational_by_i32() {
    		let r1 = Rational::new(1, 3);
    		let num = 2i32;
    
    		assert_eq!(Rational::new(1, 6), r1/num);
    	}
    
    
    	// i23 with Rationals
    
    	#[test]
    	pub fn add_rational_to_i32() {
    		let r1 = Rational::new(1, 3);
    		let num = 2i32;
    
    		assert_eq!(Rational::new(7, 3), num+r1);
    	}
    
    	#[test]
    	pub fn sub_rational_from_i32() {
    		let r1 = Rational::new(1, 3);
    		let num = 2i32;
    
    		assert_eq!(Rational::new(5, 3), num-r1);
    	}
    
    	#[test]
    	pub fn mul_i32_with_rational() {
    		let r1 = Rational::new(1, 3);
    		let num = 2i32;
    
    		assert_eq!(Rational::new(2, 3), num*r1);
    	}
    
    	#[test]
    	pub fn div_i32_by_rational() {
    		let r1 = Rational::new(1, 3);
    		let num = 2i32;
    
    		assert_eq!(Rational::new(6, 1), num/r1);
    	}
    }
    

    As you can see, we have defined a new module, rational_tests to hold our tests, and also declared the rationals crate as an external dependency (even if it’s within the same project in this case, it’s conceptually separated from out current test crate).

    We bring in that dependency using extern crate rationals;

    Let’s give it a go!

    Timmys-MacBook-Pro:rationals z0ltan$ cargo test
       Compiling rationals v0.1.0 (file:///Users/z0ltan/Rabota/Projects/Rust/rationals)
        Finished debug [unoptimized + debuginfo] target(s) in 0.33 secs
         Running target/debug/deps/rationals-0e91f0c33cf32122
    
    running 0 tests
    
    test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
    
         Running target/debug/tests-9da87246cfd0777c
    
    running 12 tests
    test rationals_tests::add_two_rationals ... ok
    test rationals_tests::div_two_rationals ... ok
    test rationals_tests::mul_i32_with_rational ... ok
    test rationals_tests::div_i32_by_rational ... ok
    test rationals_tests::div_rational_by_i32 ... ok
    test rationals_tests::mul_rational_with_i32 ... ok
    test rationals_tests::add_i32_to_rational ... ok
    test rationals_tests::add_rational_to_i32 ... ok
    test rationals_tests::mul_two_rationals ... ok
    test rationals_tests::sub_i32_from_rational ... ok
    test rationals_tests::sub_rational_from_i32 ... ok
    test rationals_tests::sub_two_rationals ... ok
    
    test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured
    
       Doc-tests rationals
    
    running 0 tests
    
    test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
    

    Wonderful!

    Closing comments

    Top

    Rust so far has been a pleasure to work with. It has its own quirks – no function overloading, a very convoluted way of doing basic I/O, and very strict borrow semantics (even in some cases where it is absolutely safe, the borrow checker will not allow uses), but overall, it is very well designed indeed.

    Prior to Rust, I had tinkered a bit with Nim, and I absolutely loved that language, but unfortunately, the language became more complicated and less consistent as soon as one began attempting to use it in real-world scenarios. I don’t see the same dissonance in Rust (so far). Yes, Rust is definitely much more verbose than Nim, but for the most part, despite its strengths as a Systems-level language, Rust feels like a very high-level language indeed that has a very strong and consistent overarching architecture.

    What I love about Rust thus far – traits (I can’t believe how useful they really are!), its quirky error handling (very Go-esque at first glance, but much more powerful in my opinion. Also, most mainstream imperative languages are moving towards Functional style error handling –

    Optional<T>

    in Java, for instance), its speed (sans garbage collection!), wonderful thread support (how I wish thread interruption was built into the Thread type itself, though), operator overloading (albeit limited), its extremely powerful pattern matching (every language needs this, especially Common Lisp!), and of course, the safety guarantees.

    I’m sure I’ll be using Rust a lot more in the near future, and not just for tooling purposes.

    Advertisements
Implementing a Rational type in Rust

2 thoughts on “Implementing a Rational type in Rust

  1. I discovered recently the Into<T> trait, one can use to convert, in your case, an arbitrary integer to a Rational. With that you can support all the integers with a single method implementation.

    Like

Speak your mind!

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s