Skip to main content
Donate to support Ukraine's independence.

Learning Rust: porting a simple Java exercise to Rust

Many of the IT people working in enterprise settings are familiar with classical OOP (Object Oriented Paradigm) and languages built with OOP in mind, like Java, C#, Python and C++. However these years functional programming and languages focused on memory safety are emerging and becoming popular. One of the languages which is emerging is Rust, which focuses on memory safety and “fearless concurrency” (cit.); you can still use it with OOP in mind, but with some caveats.

Some information about Rust

Rust uses two mechanism for dealing with memory safety: an ownership/borrowing mechanism and smart pointers which implement specific traits and dictate how pointers to memory behave.

Ownership and borrowing

Each location in memory in Rust can have a single owner at a time, even across thread; once that owner goes out of scope, the location is no longer accessible and memory is freed. Ownership can be transfered during program execution when we simply assign a variable to another. If we don’t want to transfer ownership, we can use borrowing. A memory location (some data) in Rust can have a single owner, but can be immutably borrowed to many other variable identifiers and functions. We can also borrow with mutability, but in this case the compiler assures that there is only one mutable reference.

No data races
To ensure that there are no data races (even concurrently), if a variable is borrowed mutably, no other borrowing is possible and the owner is frozen (it cannot access to the data until the borrowing terminates). Moreover if the ownership is mutable and the variable gets borrowed, the owner is again frozen until the borrowing terminates.

You can find more details here.

Smart pointers

Smart pointers are used to allocate data on the heap and force specific politics for handling the memory. They are necessary in various cases, for example for implementing recursive ADTs. There are various type of smart pointers, such as Box<T>, Rc<T>, RefCell<T>, Weak<T>, Mutex<T> and Arc<T>. Since this blog post is not focused on telling you every detail about Rust, I’ll point you to the manual if you want to dwelve deeper into these details.

A simple Java exercise

In order to play with polymorphis and dynamic dispatch let’s consider an Article interface, which forces objects to implement some methods to get the name, the price and a stringified version of the article; each specific kind of article (product, houses, whatever), must implement these methods.

public interface Article {
	public String getName();
	public double getPrice();
	public String toString();
}
public class Product implements Article {

	private String name;
	private double price;

	public Product(String name, double price) {
		this.name = name;
		this.price = price;
	}


	@Override
	public String getName() {
		return name;
	}

	@Override
	public double getPrice() {
		return price;
	}

	@Override
	public String toString() {
		return "Product [name=" + name + ", price=" + price + "]";
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Product other = (Product) obj;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		return true;
	}

}

We can implement an House class in similar fashion. Now let’s code a Catalogue containing a list of generic articles implementing the Article interface, with a method for getting the articles with a price lower than one supplied by the user via a method parameter.

import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;

import interfaces.Article;

public class Catalogue<E extends Article> {
	private List<E> art;

	public Catalogue() {
		art = new LinkedList<>(); //using LinkedList, but even ArrayList could be ok for our purposes
	}

	public void addArticle(E artble) {
		if(!art.contains(artble)) //if it already contains the article, do nothing
			art.add(artble);
	}

	public List<E> getArticlesLowerThanPrice(double price) {
		return art.stream().filter(a -> a.getPrice() < price).collect(Collectors.toCollection(LinkedList::new)); //forcing the use of a LinkedList
//		return art.stream().filter(a -> a.getPrice() < price).toList(); //use this line if you want to return an ArrayList
	}

	@Override
	public String toString() {
		StringBuilder strBuilder = new StringBuilder();

		strBuilder.append("Catalogue:\n");

		for(E a : art) {
			strBuilder.append(a.toString() + "\n");
		}

		return strBuilder.toString();
	}

}

Now our main could look like this:

import java.util.List;

import classes.House;
import classes.Catalogue;
import classes.Product;
import interfaces.Article;

public class Main {

	public static void main(String[] args) {
		Catalogue<Article> catalogue = new Catalogue<>();
		catalogue.addArticle(new Product("Salmon", 2.20d));
		catalogue.addArticle(new Product("Salmon", 4.20d)); // in our implementation, it does nothing, since the Salmon is already present
		catalogue.addArticle(new Product("Wurstel Aia", 3.20d));
		catalogue.addArticle(new Product("Parmigiano Reggiano", 10.20d));
		catalogue.addArticle(new Product("Philosopher's Stone", Double.MAX_VALUE));

		double price = 5.0d;
		System.out.println(catalogue);
		List<Article> articlesBelowPrice = catalogue.getArticlesLowerThanPrice(price);

		System.out.println("Article with a price lower than " + price);
		for(Article art : articlesBelowPrice)
			System.out.println(art);

		Catalogue<Article> houses = new Catalogue<>();
		houses.addArticle(new House("Via Amerigo Vespucci 30, Roma", 125000.0d));
		houses.addArticle(new House("Via Amerigo Vespucci 30, Roma", 130000.0d)); // in our implementation, it does nothing, since the Salmon is already present
		houses.addArticle(new House("Via Roma 50, Roma", 180000.0d));
		houses.addArticle(new House("Via John Doe 80, Roma", 280000.0d));

		System.out.println();
		System.out.println(houses);

		price = 190000.0d;
		List<Article> housesBelowPrice = houses.getArticlesLowerThanPrice(price);

		System.out.println("Houses with a cost lower than " + price);
		for(Article house : housesBelowPrice)
			System.out.println(house);
	}

}

If you run the previous code, you should get something like this:

Catalogue:
Product [name=Salmone, price=2.2]
Product [name=Wurstel Aia, price=3.2]
Product [name=Parmigiano Reggiano, price=10.2]
Product [name=Philosopher's Stone, price=1.7976931348623157E308]

Article with a price lower than 5.0
Product [name=Salmone, price=2.2]
Product [name=Wurstel Aia, price=3.2]

Catalogue:
House [address=Via Amerigo Vespucci 30, Roma, price=125000.0]
House [address=Via Roma 50, Roma, price=180000.0]
House [address=Via John Doe 80, Roma, price=280000.0]

Houses with a cost lower than 190000.0
House [address=Via Amerigo Vespucci 30, Roma, price=125000.0]
House [address=Via Roma 50, Roma, price=180000.0]

Porting the previous code to Rust

Important
This is a personal approach to the problem, I’m still learning to use Rust, so there may be cleaner and more efficient solutions. However I wanted to share the journey with you, since some concepts may be useful to other people out there.

First of all, we need a trait (something like Java interfaces, quoting the manual, “they define functionalities a particular type has and can share with others”).

use std::fmt::Display;

pub trait Article: Display {
    fn get_name(&self) -> String;
    fn get_price(&self) -> f64;
    fn clone_dyn(&self) -> Box<dyn Article>;
}

More details on the Box and clone_dyn later. Now let’s define a Product; in Rust there are no classes, since it uses structs. Each struct can have an implementation and implement some traits. For example, in our case:

#[derive(Clone)]
pub struct Product {
    name: String,
    price: f64
}

impl Product {
    pub fn new(name: String, price: f64) -> Self {
        Self {
            name,
            price
        }
    }

    pub fn set_name(&mut self, name: String) {
        self.name = name;
    }

    pub fn set_price(&mut self, price: f64) {
        self.price = price;
    }
}

impl fmt::Display for Product {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Product [name: {}, price: {}]", self.name, self.price)
    }
}

impl Article for Product {
    fn get_name(&self) -> String {
        self.name.clone()
    }

    fn get_price(&self) -> f64 {
        self.price
    }

    fn clone_dyn(&self) -> Box<dyn Article> {
        Box::new(self.clone()) // Forward to the derive(Clone) impl
    }
}

As you can see, we used an “associated function” (the new function) as a constructor and then defined the setters as methods. Then we implemented the Article trait for the Product and the Display trait (similar to Java’s toString). Again we can have an House struct implemented in similar fashion.

Now we must implement the Catalogue struct and here come the problems: we can’t define a generic struct Catalogue<T: Article> with a Vec<T>. Why? Because the compiler needs to know how much memory to allocate and Article is simply a trait, not a concrete object type. So what do we do? The solutions are trait objects and boxing.

Box ’em, box ’em all

In our case we can solve the problem by declaring a struct with a field which is a Vec<Box<dyn Article>>, telling Rust to use a vector of boxed trait objects. By using Box<T> we allocate memory on the heap for the enclosed type, while on the stack there is a fixed size pointer; et voilà, now Rust knows how much memory to allocate on the stack.

use std::fmt;
use crate::traits::Article::Article;

impl Clone for Box<dyn Article> {
    fn clone(&self) -> Self {
        self.clone_dyn()
    }
}

pub struct Catalogue {
    articles: Vec<Box<dyn Article>>,
}

pub struct WrapperVec(pub Vec<Box<dyn Article>>);

impl Catalogue{
    pub fn new() -> Self {
        Self {
            articles: Vec::new(),
        }
    }

    pub fn add_article(&mut self, article: Box<dyn Article>) {
        self.articles.push(article);
    }

    pub fn get_articles_belowprice(&self, price: f64) -> Vec<Box<dyn Article>> {
        self.articles.iter().filter(|art| art.get_price() < price).cloned().collect()
    }
}

impl fmt::Display for Catalogue {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Catalogue:\n{}", self.articles.iter().map(|art| art.to_string()).collect::<Vec<String>>().join("\n"))
    }
}

impl fmt::Display for WrapperVec {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Catalogue:\n{}", self.0.iter().map(|art| art.to_string()).collect::<Vec<String>>().join("\n"))
    }
}

In fn get_articles_belowprice we need the cloned() since otherwise Rust will complain with the following message:

error[E0277]: a value of type `Vec<Box<dyn Article>>` cannot be built from an iterator over elements of type `&Box<dyn Article>`
    --> src\classes\catalogue.rs:28:9
     |
28   |         self.articles.iter().filter(|art| art.get_price() < price).collect()
     |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ------- required by a bound introduced by this call
     |         |
     |         value of type `Vec<Box<dyn Article>>` cannot be built from `std::iter::Iterator<Item=&Box<dyn Article>>`
     |
     = help: the trait `FromIterator<&Box<dyn Article>>` is not implemented for `Vec<Box<dyn Article>>`
     = help: the trait `FromIterator<T>` is implemented for `Vec<T>`
note: required by a bound in `collect`
    --> C:\Users\darthvi\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib/rustlib/src/rust\library\core\src\iter\traits\iterator.rs:1832:19
     |
1832 |     fn collect<B: FromIterator<Self::Item>>(self) -> B
     |                   ^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `collect`

This is the reason why we set up an implementation of Clone for Box<dyn Article>.

The newtype pattern

If you read the previous snippet of code carefully, you should notice the presence of a wrapper WrapperVec. Why do we need it? Well, we return a Vec<Box<dyn Article> when we want to get articles below a specific price, but we can’t implement fmt::Display for this type, since it’s not in the current crate.

Searching for solution on the web, I found out that one of the best practices in this case is to use the newtype pattern. More specifically we declare a new struct with an inner field of another type and then we implement the fmt::Display for this new type. And that’s it.

The main

Now we can code the main, like this:

use classes::catalogue::Catalogue;
use classes::product::Product;

use crate::classes::catalogue::WrapperVec;

mod traits;
mod classes;

fn main() {
    let mut catalogue = Catalogue::new();
    catalogue.add_articles(Box::new(Product::new("Salmone".to_string(), 3.50)));
    catalogue.add_articles(Box::new(Product::new("Wurstel".to_string(), 1.50)));
    catalogue.add_articles(Box::new(Product::new("Parmigiano Reggiano".to_string(), 9.50)));

    println!("{}", catalogue);

    let belowprice = catalogue.get_articles_belowprice(5.0);

    let wrapper = WrapperVec(belowprice);

    println!("{}", wrapper);

}

The result of running it should look like this:

Catalogue:
Product [name: Salmone, price: 3.5]
Product [name: Wurstel, price: 1.5]
Product [name: Parmigiano Reggiano, price: 9.5]
Catalogue:
Product [name: Salmone, price: 3.5]
Product [name: Wurstel, price: 1.5]

So here we are, using dynamic dispatch on a trait object in Rust instead of static dispatch and monomorphization. I hope you find this blog post interesting and useful.

Send a like: