Rust logic and traits

Alberto Basalo
5 min readApr 24, 2024

--

One of the most cryptic aspects of Rust is its system of traits and implementation of functionality for associating methods with structures. To understand how it works, I propose, as an example, developing a simple blockchain that allows algorithms to be applied to data structures.

Implementation of a blockchain in Rust to learn traits

Pre-requirements

To follow this tutorial, you need to have a basic knowledge of Rust. If not, I recommend that you read my previous tutorials on Rust.

Now, let’s get to the important thing. I start with the link to the full code for the impatient.

Data and logic

Rust data structures focus on defining the information they store but not on the logic that is applied to that information.

To do this, functions that act on the data structures can be defined, as we saw in previous examples. But, a key to programming is putting together those things that are related, and separating those that are not.

For example, we see the Blockchain structure, which lacks functionality.

/// A Struct to represent a **chain** of [`Block`] nodes.
struct Blockchain {
/// The nodes of the chain as a vector of [`Block`] structs.
blocks: Vec<Block>,
/// The timestamp of the last change.
timestamp: u128,
/// A calculated hash used to self validate.
hash: String,
}

Add functionality to a data type

In Rust, you can add functionality to an existing data type using an implementation. This allows the capabilities of a data type to be added without modifying its original definition.

For example, in this case, functionality is implemented for the Blockchain structure so that it can create a new block for the chain, check if it is valid, and obtain the latest block.

/// Implement functionality the `Block` struct.
impl Blockchain {
/// Creates a new blockchain with a genesis block.
fn new() -> Blockchain {
let mut blockchain = Blockchain {
blocks: vec![],
timestamp: get_timestamp(),
hash: "".to_string(),
};
blockchain.mine("Genesis block".to_string());
blockchain.sign();
println!("✨ Created a new blockchain {:#?}", blockchain);
blockchain
}
/// Adds a [`Block`] to the [`Blockchain`].
/// - The [`Blockchain`] hash is updated after adding the block.
/// - The block is only added to the [`Blockchain`] if it is valid.
fn add_block(&mut self, block: Block) {
let block_clone = block.clone();
self.blocks.push(block);
self.timestamp = get_timestamp();
self.hash = self.sign();
if self.is_valid() {
println!("📘 Added block {:#?}", block_clone);
} else {
println!("📕 Removing invalid Block {:#?}", block_clone);
self.blocks.pop();
}
}
/// Returns the last block of the [`Blockchain`]
/// - Being an [`Option`], it returns none when the [`Blockchain`] is empty.
fn last_block(&self) -> Option<Block> {
if self.blocks.is_empty() {
return None;
} else {
let last_block: Block = self.blocks[self.blocks.len() - 1].clone();
return Some(last_block);
}
}
}

Similarities and differences with Object Oriented Programming

This pair of concepts, struct, and impl, are similar to classes and methods in Object-Oriented Programming. However, Rust is not an object-oriented language and does not use concepts such as inheritance, while polymorphism treats it very differently.

Introduction to Traits

Traits are a fundamental feature of Rust that allows you to define common behaviors for different types of data. Once defined, generic or specific implementations can be developed for certain types of data.

Traits in Rust are similar to interfaces in other programming languages, such as Java or C#, but focus more on logic than data.

Definition of Traits

A Trait is declared with the keyword trait followed by the name and a list of methods that define the specific behavior of any data type that implements it.

For example, both the blockchain and each of its nodes must be signed and validated, so a Trait Signature can be defined that defines the sign and is_valid methods.

/// Sign and validate structs where it is applied.
trait Signature {
/// Signs the struct returning a calculated hash of its content and metadata.
fn sign(&self) -> String;
/// Checks if the struct is valid by returning a boolean.
fn is_valid(&self) -> bool;
}

Traits Implementation

To implement a Trait, the keyword impl is followed by the name and the type of data it is being implemented. Below is an example of how the Signature Trait is implemented for the Block structure.

/// Implement the [`Signature`] trait for the [`Block`] struct.
impl Signature for Block {
/// Signs a block by hashing it.
fn sign(&self) -> String {
let mut hasher = DefaultHasher::new();
self.hash(&mut hasher);
format!("{:x}", hasher.finish())
}
/// Checks if the block is valid by recalculating the hash
fn is_valid(&self) -> bool {
let hash = self.sign();
if self.hash != hash {
println!(
"💔 Block {} hash {} is not the expected {}",
self.index, self.hash, hash
);
return false;
}
true
}
}

You can consult the complete code on GitHub and see how that same trait is also implemented for the Blockchain structure.

Implementation of imported traits

In addition to implementing your own traits, you can override the behavior of existing traits. For example, you can implement the Debug trait so that the Block structure is displayed in a more user-friendly way.

/// Implement the [`Debug`] trait for the [`Block`] struct.
impl fmt::Debug for Block {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
F,
"Block {} created at timestamp: {}, signed with hash: {} }}",
self.index, self.timestamp, self.hash
)
}
}

Likewise, you can make the Blockchain structure printable by asking Rust to derive the Debug trait for it.

/// A Struct to represent a **chain** of [`Block`] nodes.
#[derive(Debug)]
struct Blockchain {
/// The nodes of the chain as a vector of [`Block`] structs.
blocks: Vec<Block>,
/// The timestamp of the last change.
timestamp: u128,
/// A calculated hash used to self validate.
hash: String,
}

Use of Traits

Once implemented, its methods can be invoked on any instance of the structure to which they are applied. They can also be used as a data type in arguments and returns of functions and methods using the dyn instruction.

As an example, I show you a function that checks and prints whether a block or chain of blocks is valid or not.

/// Utility function to check if a [`Signature`] is valid
/// - Prints a message with the result.
fn check_signature(signature: &dyn Signature) -> bool {
if signature.is_valid() {
println!("💚 The signature is valid");
true
} else {
println!("💔 The signature is not valid");
false
}
}

Conclusions

Rust traits for defining and implementing logic

Rust is a multi-paradigm programming language, but it is not especially object-oriented. Instead, offer the trait system as a powerful feature that allows you to define common or specific behaviors for different types of data.

--

--

Alberto Basalo

Advisor and instructor for developers. Keeping on top of modern technologies while applying proven patterns learned over the last 25 years. Angular, Node, Tests