Day 4: Printing Custom types
Welcome back, fellow Rustaceans 🦀!
Last time, we explored formatting options provided by the Rust std::fmt
library. We saw how placeholders make it easy to print different types without needing C-style formatters like %d
. Additionally, we learned how to use features like {:x}
to customize the output format (e.g., hexadecimal). Finally, we got a glimpse of the Debug trait and how it allows us to automatically derive default formatting for custom data types.
Today, we'll delve deeper into Debug and Display, Rust's way of extending formatted prints to our custom data types.
So, you might be wondering why the ability to print custom data types is so important. Printing custom data types like structures is crucial for debugging, analysis, and user communication. During the development process, printing the contents of a structure offers invaluable insights into the program's internal state. This allows you to track variable values, verify that your code manipulates data correctly, and pinpoint the root of potential errors. Additionally, printing the contents of structures can be used to generate logs or reports that present complex information in a human-readable format, enhancing the overall understanding of the program's output.
Okay, let's take a look at how we would print a custom data type in C.
#include <stdio.h>
// Define the structure
struct Book {
char title[50];
char author[50];
int year;
};
int main() {
// Create a structure variable
struct Book myBook = {"The Hobbit", "J.R.R. Tolkien", 1937};
// Print the structure members
printf("Title: %s\n", myBook.title);
printf("Author: %s\n", myBook.author);
printf("Year: %d\n", myBook.year);
return 0;
}
Here we start by defining the Book
structure with members title
, author
, and year
to represent the information about a book. In the main
function, we create a Book
variable named myBook
and initialize it with sample data. We use printf
, and the dot operator (.
) to access and print the individual members of the myBook
structure. The %s
format specifier is used for strings (title and author), and %d
is used for the integer (year).
While the C approach offers basic functionality, it has limitations. Manually accessing and printing each member can become tedious. Obviously, each of the files defining these structures could define a custom function that takes the structure to be printed as an argument and then handles the printing. The problem with this approach is that it lacks universality and makes it difficult to achieve consistency. Since this is a common use case for debugging and logging, most modern languages try to provide a unified solution. Rust, like many modern languages, abstracts this using the Debug
, and Display
traits.
As dictated in the previous article you can think of traits as a way for Rust to define behavior like how to print a specific type of data. Rust's Debug
and Display
traits provide a more elegant solution. By implementing these traits for your structures, you can print them using a single placeholder ({:?}
for Debug
and {}
for Display
). This centralizes formatting logic and improves readability. Moreover, you can customize the output format for different use cases. Debug
provides a detailed, internal representation suitable for debugging, while Display
allows for user-friendly output control. This separation of concerns simplifies printing and promotes cleaner, more maintainable code.
Debug Trait
The Debug trait, as the name suggests, is perfect for debugging scenarios. Let's say you're working through some code and want a quick way to print all the elements of a structure or another custom type. The Debug trait is your solution! Rust makes it even easier by automatically generating a Debug implementation using the #[derive(Debug)]
attribute. This trait focuses on providing a detailed, technical view of a data structure, making it ideal for debugging. Let's have a look at an example:
use std::fmt;
// let's rust compiler generate Debug trait for the struct
#[derive(Debug)]
struct Footballer {
name: String,
country: String,
goals_scored: u32,
matches_played: u32,
trophies_won: Vec<String>, // To store a list of trophies
}
fn main() {
let player = Footballer {
name: "Lionel Messi".to_string(),
country: "Argentina".to_string(),
goals_scored: 796, // Updated goal count
matches_played: 1073, // Updated match count
trophies_won: vec!["Champions League".to_string(), "La Liga".to_string(), "Copa del Rey".to_string(), "World Cup".to_string()],
};
println!("{:?}", player);
}
I know we haven't looked into structures in Rust yet, but for a moment, let's try to forget about the format. This defines a structure named Footballer
. Structures in Rust allow us to group related data together. This struct
represents a footballer and contains the following fields:
- name: String: The player's name.
- country: String: The player's country of origin.
- goals_scored: u32: The number of goals the player has scored (
u32
is an unsigned 32-bit integer). - matches_played: u32: The number of matches the player has participated in.
- trophies_won: Vec<String>: A vector (flexible list) of strings to store the trophies won by the player.
#[derive(Debug)]
: This is the key part! It instructs the Rust compiler to automatically generate an implementation of the Debug
trait for our Footballer
struct
. With #[derive(Debug)]
, we can now print a Footballer
instance using the {:?}
placeholder for Debug formatting.
You should see something like this:
Footballer { name: "Lionel Messi", country: "Argentina", goals_scored: 796, matches_played: 1073, trophies_won: ["Champions League", "La Liga", "Copa del Rey", "World Cup"] }
Footballer {
name: "Lionel Messi",
country: "Argentina",
goals_scored: 796,
matches_played: 1073,
trophies_won: [
"Champions League",
"La Liga",
"Copa del Rey",
"World Cup",
],
}
Great, this looks much more readable! The Debug trait is perfect for quick debugging sessions when you need to inspect the internal details of a structure or payload. However, it might not be ideal for user-facing logs. In production environments, you often want control over what information is displayed, and you might want to add prefixes or suffixes to make the messages easier for users to understand. Rust provides a solution for this using the Display trait.
Display Trait
Unlike the Debug trait, the Display trait doesn't come automatically. Rust wants you to provide a custom implementation for the Display trait. This makes sense, right? If you weren't happy with Rust's auto-generated Debug output, it's logical that you'd want to define your own Display behavior.
So, let's roll up our sleeves and implement the Display trait for our custom type!
Let's go to the official Rust documentation at https://doc.rust-lang.org/ and look for std::fmt::Display
. One thing you'll notice is that the Rust standard library has some awesome documentation. It not only explains the concept but also provides a working example.
So, based on the documentation, here's how we can implement Display for our Footballer struct:
impl fmt::Display for Footballer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({}) - Goals: {}, Matches: {}, Trophies: {}",
self.name, self.country, self.goals_scored, self.matches_played, self.trophies_won.join(", "))
}
}
We won't worry too much about the exact syntax for now. The main idea is that whenever Rust sees the {}
placeholder, it calls the Display trait for the type being printed. It gives the trait implementation two pointers: one to the formatter buffer (where the output goes) and another to the structure itself. This gives the developer full control to format and represent the structure's data however they want, and then write that formatted output to the buffer using the write!
macro. That's exactly what we're doing in the line below.
write!(f, "{} ({}) - Goals: {}, Matches: {}, Trophies: {}",
self.name, self.country, self.goals_scored, self.matches_played, self.trophies_won.join(", "))
Let's try out our new implementation! We will replace the Debug placeholder {:?}
with Display placeholder {}
.
use std::fmt;
// let's rust compiler generate Debug trait for the struct
#[derive(Debug)]
struct Footballer {
name: String,
country: String,
goals_scored: u32,
matches_played: u32,
trophies_won: Vec<String>, // To store a list of trophies
}
impl fmt::Display for Footballer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({}) - Goals: {}, Matches: {}, Trophies: {}",
self.name, self.country, self.goals_scored, self.matches_played, self.trophies_won.join(", "))
}
}
fn main() {
let player = Footballer {
name: "Lionel Messi".to_string(),
country: "Argentina".to_string(),
goals_scored: 796, // Updated goal count
matches_played: 1073, // Updated match count
trophies_won: vec!["Champions League".to_string(), "La Liga".to_string(), "Copa del Rey".to_string(), "World Cup".to_string()],
};
println!("{}", player);
}
You should see something like this:
Lionel Messi (Argentina) - Goals: 796, Matches: 1073, Trophies: Champions League, La Liga, Copa del Rey, World Cup
Yeah, this looks way better for a production log!
Conclusion
Fellow Rustaceans 🦀, We should be proud of ourselves, we've come a long way! Today, we took our printing skills to the next level by exploring the Debug and Display traits. Understanding how to customize the output of your custom data types is crucial when it comes to debugging, logging, and presenting information in a clear, user-friendly way.
You can find all the code examples from today's session on the inpyjama GitHub page: https://github.com/inpyjama/rust/tree/main/day4.
See you tomorrow!
Discussion