Struct and Enum Exercises
Master Rust's type system with these exercises covering struct definitions, methods, enums, pattern matching, and real-world modeling.
Exercise 1: Basic Rectangle
Difficulty: Easy
Problem: Create a Rectangle struct and implement methods to calculate area and perimeter.
Requirements:
- Define a struct with width and height
- Implement
area()method - Implement
perimeter()method - Implement
can_hold()to check if another rectangle fits inside
Example:
let rect = Rectangle::new(10, 20);
println!("Area: {}", rect.area()); // 200
println!("Perimeter: {}", rect.perimeter()); // 60
Hints:
- Use
implblock for methods - Use
&selffor methods that don't modify the struct - Area = width × height, Perimeter = 2 × (width + height)
Solution
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn new(width: u32, height: u32) -> Self {
Rectangle { width, height }
}
fn area(&self) -> u32 {
self.width * self.height
}
fn perimeter(&self) -> u32 {
2 * (self.width + self.height)
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width >= other.width && self.height >= other.height
}
fn is_square(&self) -> bool {
self.width == self.height
}
}
fn main() {
let rect1 = Rectangle::new(10, 20);
let rect2 = Rectangle::new(5, 10);
println!("Rectangle: {:?}", rect1);
println!("Area: {}", rect1.area());
println!("Perimeter: {}", rect1.perimeter());
println!("Is square: {}", rect1.is_square());
println!("Can hold rect2: {}", rect1.can_hold(&rect2));
}
Learning Points:
- Defining structs with named fields
- Associated functions (like
new) - Methods with
&self - Deriving
Debugtrait
Exercise 2: Point and Distance
Difficulty: Easy
Problem: Create a Point struct and calculate distance between two points.
Requirements:
- Create a 2D point struct
- Implement
distance_from_origin()method - Implement
distance_to()method for distance to another point - Use floating-point coordinates
Example:
let p1 = Point::new(0.0, 0.0);
let p2 = Point::new(3.0, 4.0);
println!("Distance: {}", p1.distance_to(&p2)); // 5.0
Hints:
- Distance formula: √((x2-x1)² + (y2-y1)²)
- Use
f64::sqrt()for square root - Use
.powi(2)for squaring
Solution
#[derive(Debug, Clone, Copy)]
struct Point {
x: f64,
y: f64,
}
impl Point {
fn new(x: f64, y: f64) -> Self {
Point { x, y }
}
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
fn distance_to(&self, other: &Point) -> f64 {
let dx = self.x - other.x;
let dy = self.y - other.y;
(dx.powi(2) + dy.powi(2)).sqrt()
}
fn midpoint(&self, other: &Point) -> Point {
Point {
x: (self.x + other.x) / 2.0,
y: (self.y + other.y) / 2.0,
}
}
}
fn main() {
let p1 = Point::new(0.0, 0.0);
let p2 = Point::new(3.0, 4.0);
println!("p1: {:?}", p1);
println!("p2: {:?}", p2);
println!("Distance from origin: {}", p2.distance_from_origin());
println!("Distance between points: {}", p1.distance_to(&p2));
println!("Midpoint: {:?}", p1.midpoint(&p2));
}
Learning Points:
- Deriving
CopyandClonefor simple types - Mathematical operations in Rust
- Methods that return new instances
Exercise 3: Traffic Light Enum
Difficulty: Easy
Problem: Create a TrafficLight enum and implement behavior for each state.
Requirements:
- Define enum with Red, Yellow, Green variants
- Implement
duration()method that returns how long each light lasts - Implement
next()method that returns the next light in the sequence
Example:
let light = TrafficLight::Red;
println!("Duration: {} seconds", light.duration());
println!("Next: {:?}", light.next());
Hints:
- Use
matchfor pattern matching - Each variant can have different behavior
- Use
&selffor methods
Solution
#[derive(Debug, Clone, Copy, PartialEq)]
enum TrafficLight {
Red,
Yellow,
Green,
}
impl TrafficLight {
fn duration(&self) -> u32 {
match self {
TrafficLight::Red => 60,
TrafficLight::Yellow => 5,
TrafficLight::Green => 55,
}
}
fn next(&self) -> TrafficLight {
match self {
TrafficLight::Red => TrafficLight::Green,
TrafficLight::Yellow => TrafficLight::Red,
TrafficLight::Green => TrafficLight::Yellow,
}
}
fn can_cross(&self) -> bool {
match self {
TrafficLight::Red => false,
TrafficLight::Yellow => false,
TrafficLight::Green => true,
}
}
}
fn main() {
let mut light = TrafficLight::Red;
for _ in 0..5 {
println!("{:?}: {} seconds, can cross: {}",
light, light.duration(), light.can_cross());
light = light.next();
}
}
Learning Points:
- Defining enums without data
- Pattern matching with
match - Deriving
PartialEqfor comparison - Methods on enums
Exercise 4: Shape Enum with Data
Difficulty: Medium
Problem: Create a Shape enum that can represent circles, rectangles, and triangles with their dimensions.
Requirements:
- Each variant should store appropriate data
- Implement
area()method for each shape - Implement
perimeter()method
Example:
let circle = Shape::Circle(5.0);
let rect = Shape::Rectangle(10.0, 20.0);
println!("Circle area: {}", circle.area());
Hints:
- Use tuple-like or struct-like variants
- Match on variants and extract data
- π ≈ 3.14159 or use
std::f64::consts::PI
Solution
use std::f64::consts::PI;
#[derive(Debug)]
enum Shape {
Circle(f64), // radius
Rectangle(f64, f64), // width, height
Triangle(f64, f64, f64), // three sides
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle(radius) => PI * radius * radius,
Shape::Rectangle(width, height) => width * height,
Shape::Triangle(a, b, c) => {
// Heron's formula
let s = (a + b + c) / 2.0;
(s * (s - a) * (s - b) * (s - c)).sqrt()
}
}
}
fn perimeter(&self) -> f64 {
match self {
Shape::Circle(radius) => 2.0 * PI * radius,
Shape::Rectangle(width, height) => 2.0 * (width + height),
Shape::Triangle(a, b, c) => a + b + c,
}
}
}
fn main() {
let shapes = vec![
Shape::Circle(5.0),
Shape::Rectangle(10.0, 20.0),
Shape::Triangle(3.0, 4.0, 5.0),
];
for shape in shapes {
println!("{:?}", shape);
println!(" Area: {:.2}", shape.area());
println!(" Perimeter: {:.2}", shape.perimeter());
}
}
Learning Points:
- Enum variants with different types of data
- Pattern matching to extract data
- Using constants from standard library
- Heron's formula for triangle area
Exercise 5: Option and Result Practice
Difficulty: Medium
Problem: Create a Person struct with optional middle name and implement safe division.
Requirements:
- Use
Option<String>for middle name - Implement
full_name()method - Create
divide()function that returnsResult - Handle division by zero
Example:
let person = Person::new("John", Some("Michael"), "Doe");
println!("{}", person.full_name());
Hints:
- Use
Optionfor values that may not exist - Use
Result<T, E>for operations that can fail - Pattern match or use combinators
Solution
#[derive(Debug)]
struct Person {
first_name: String,
middle_name: Option<String>,
last_name: String,
}
impl Person {
fn new(first: &str, middle: Option<&str>, last: &str) -> Self {
Person {
first_name: first.to_string(),
middle_name: middle.map(|s| s.to_string()),
last_name: last.to_string(),
}
}
fn full_name(&self) -> String {
match &self.middle_name {
Some(middle) => format!("{} {} {}", self.first_name, middle, self.last_name),
None => format!("{} {}", self.first_name, self.last_name),
}
}
fn initials(&self) -> String {
let first = self.first_name.chars().next().unwrap();
let last = self.last_name.chars().next().unwrap();
match &self.middle_name {
Some(middle) => {
let middle_initial = middle.chars().next().unwrap();
format!("{}.{}.{}.", first, middle_initial, last)
}
None => format!("{}.{}.", first, last),
}
}
}
#[derive(Debug)]
enum MathError {
DivisionByZero,
}
fn divide(a: f64, b: f64) -> Result<f64, MathError> {
if b == 0.0 {
Err(MathError::DivisionByZero)
} else {
Ok(a / b)
}
}
fn main() {
let person1 = Person::new("John", Some("Michael"), "Doe");
let person2 = Person::new("Jane", None, "Smith");
println!("{}", person1.full_name());
println!("{}", person2.full_name());
println!("{}", person1.initials());
match divide(10.0, 2.0) {
Ok(result) => println!("10 / 2 = {}", result),
Err(e) => println!("Error: {:?}", e),
}
match divide(10.0, 0.0) {
Ok(result) => println!("10 / 0 = {}", result),
Err(e) => println!("Error: {:?}", e),
}
}
Learning Points:
- Using
Optionfor optional fields - Using
Resultfor error handling - Pattern matching on Option and Result
- Custom error types
Exercise 6: Linked List Node
Difficulty: Hard
Problem: Implement a simple singly linked list node using enums.
Requirements:
- Use
Boxfor heap allocation - Each node contains data and optional next node
- Implement methods to add and traverse
Example:
let mut list = List::new();
list.push(1);
list.push(2);
list.push(3);
Hints:
- Use
Option<Box<Node>>for the next pointer Boxallows recursive types- Implement iteratively to avoid deep recursion
Solution
#[derive(Debug)]
struct Node<T> {
data: T,
next: Option<Box<Node<T>>>,
}
#[derive(Debug)]
struct List<T> {
head: Option<Box<Node<T>>>,
}
impl<T> List<T> {
fn new() -> Self {
List { head: None }
}
fn push(&mut self, data: T) {
let new_node = Box::new(Node {
data,
next: self.head.take(),
});
self.head = Some(new_node);
}
fn pop(&mut self) -> Option<T> {
self.head.take().map(|node| {
self.head = node.next;
node.data
})
}
fn peek(&self) -> Option<&T> {
self.head.as_ref().map(|node| &node.data)
}
}
impl<T: std::fmt::Display> List<T> {
fn print(&self) {
let mut current = &self.head;
while let Some(node) = current {
print!("{} -> ", node.data);
current = &node.next;
}
println!("None");
}
}
fn main() {
let mut list = List::new();
list.push(1);
list.push(2);
list.push(3);
println!("List:");
list.print();
println!("Peek: {:?}", list.peek());
println!("Pop: {:?}", list.pop());
println!("After pop:");
list.print();
}
Learning Points:
- Using
Boxfor heap-allocated recursive types Optionfor representing null pointerstake()method to move out of an Option- Generic types with trait bounds
Exercise 7: JSON-like Value Enum
Difficulty: Medium
Problem: Create an enum to represent JSON-like values.
Requirements:
- Support Null, Bool, Number, String, Array, Object
- Implement
to_string()method for each variant - Support nested structures
Example:
let value = JsonValue::Object(vec![
("name", JsonValue::String("John")),
("age", JsonValue::Number(30)),
]);
Hints:
- Use
HashMaporVec<(String, JsonValue)>for objects - Make it recursive for arrays and objects
- Use
Boxif needed for size
Solution
use std::collections::HashMap;
#[derive(Debug, Clone)]
enum JsonValue {
Null,
Bool(bool),
Number(f64),
String(String),
Array(Vec<JsonValue>),
Object(HashMap<String, JsonValue>),
}
impl JsonValue {
fn to_string(&self) -> String {
match self {
JsonValue::Null => "null".to_string(),
JsonValue::Bool(b) => b.to_string(),
JsonValue::Number(n) => n.to_string(),
JsonValue::String(s) => format!("\"{}\"", s),
JsonValue::Array(arr) => {
let items: Vec<String> = arr.iter()
.map(|v| v.to_string())
.collect();
format!("[{}]", items.join(", "))
}
JsonValue::Object(obj) => {
let items: Vec<String> = obj.iter()
.map(|(k, v)| format!("\"{}\": {}", k, v.to_string()))
.collect();
format!("{{{}}}", items.join(", "))
}
}
}
fn get_type(&self) -> &str {
match self {
JsonValue::Null => "null",
JsonValue::Bool(_) => "boolean",
JsonValue::Number(_) => "number",
JsonValue::String(_) => "string",
JsonValue::Array(_) => "array",
JsonValue::Object(_) => "object",
}
}
}
fn main() {
let mut person = HashMap::new();
person.insert("name".to_string(), JsonValue::String("John".to_string()));
person.insert("age".to_string(), JsonValue::Number(30.0));
person.insert("active".to_string(), JsonValue::Bool(true));
let hobbies = JsonValue::Array(vec![
JsonValue::String("reading".to_string()),
JsonValue::String("coding".to_string()),
]);
person.insert("hobbies".to_string(), hobbies);
let value = JsonValue::Object(person);
println!("{}", value.to_string());
println!("Type: {}", value.get_type());
}
Learning Points:
- Complex enum structures
- Recursive types
- Pattern matching on complex enums
- Building domain-specific types
Exercise 8: State Machine
Difficulty: Medium
Problem: Model a connection state machine using enums.
Requirements:
- States: Disconnected, Connecting, Connected, Error
- Each state can transition to specific other states
- Implement
transition()method - Store state-specific data
Example:
let mut conn = Connection::new();
conn.connect("server.com");
conn.send_data("hello");
Hints:
- Use enum variants with different data
- Some transitions are invalid
- Return
Resultfor transitions
Solution
#[derive(Debug)]
enum ConnectionState {
Disconnected,
Connecting { server: String },
Connected { server: String, session_id: u32 },
Error { message: String },
}
#[derive(Debug)]
struct Connection {
state: ConnectionState,
}
impl Connection {
fn new() -> Self {
Connection {
state: ConnectionState::Disconnected,
}
}
fn connect(&mut self, server: &str) -> Result<(), String> {
match &self.state {
ConnectionState::Disconnected => {
self.state = ConnectionState::Connecting {
server: server.to_string(),
};
// Simulate connection
self.state = ConnectionState::Connected {
server: server.to_string(),
session_id: 12345,
};
Ok(())
}
_ => Err("Can only connect from disconnected state".to_string()),
}
}
fn send_data(&self, data: &str) -> Result<(), String> {
match &self.state {
ConnectionState::Connected { server, session_id } => {
println!("Sending '{}' to {} (session: {})", data, server, session_id);
Ok(())
}
_ => Err("Not connected".to_string()),
}
}
fn disconnect(&mut self) -> Result<(), String> {
match &self.state {
ConnectionState::Connected { .. } => {
self.state = ConnectionState::Disconnected;
Ok(())
}
_ => Err("Not connected".to_string()),
}
}
fn get_status(&self) -> String {
match &self.state {
ConnectionState::Disconnected => "Disconnected".to_string(),
ConnectionState::Connecting { server } => {
format!("Connecting to {}...", server)
}
ConnectionState::Connected { server, session_id } => {
format!("Connected to {} (session: {})", server, session_id)
}
ConnectionState::Error { message } => {
format!("Error: {}", message)
}
}
}
}
fn main() {
let mut conn = Connection::new();
println!("Status: {}", conn.get_status());
conn.connect("server.com").unwrap();
println!("Status: {}", conn.get_status());
conn.send_data("hello").unwrap();
conn.disconnect().unwrap();
println!("Status: {}", conn.get_status());
}
Learning Points:
- State machines with enums
- Struct-like enum variants
- Enforcing valid state transitions
- Extracting data from enum variants
Exercise 9: Generic Container
Difficulty: Medium
Problem: Create a generic Container struct that can hold any type.
Requirements:
- Use generics
- Implement methods:
new,set,get - Implement
mapto transform the value - Add trait bounds where needed
Example:
let mut container = Container::new(5);
container.set(10);
let doubled = container.map(|x| x * 2);
Hints:
- Use
<T>for generic type parameter Clonebound might be neededmapcreates a new container
Solution
#[derive(Debug)]
struct Container<T> {
value: T,
}
impl<T> Container<T> {
fn new(value: T) -> Self {
Container { value }
}
fn set(&mut self, value: T) {
self.value = value;
}
fn get(&self) -> &T {
&self.value
}
fn get_mut(&mut self) -> &mut T {
&mut self.value
}
fn into_inner(self) -> T {
self.value
}
fn map<U, F>(self, f: F) -> Container<U>
where
F: FnOnce(T) -> U,
{
Container {
value: f(self.value),
}
}
}
impl<T: Clone> Container<T> {
fn cloned(&self) -> Container<T> {
Container {
value: self.value.clone(),
}
}
}
impl<T: std::fmt::Display> Container<T> {
fn print(&self) {
println!("Container contains: {}", self.value);
}
}
fn main() {
let mut container = Container::new(5);
container.print();
container.set(10);
container.print();
let doubled = container.map(|x| x * 2);
doubled.print();
let string_container = Container::new("hello".to_string());
string_container.print();
let length = string_container.map(|s| s.len());
println!("Length: {:?}", length);
}
Learning Points:
- Generic structs
- Generic methods
- Trait bounds (
Clone,Display) - Method chaining with generics
FnOncefor closures
Exercise 10: Employee Database
Difficulty: Medium
Problem: Create an employee management system with different employee types.
Requirements:
- Base
Employeestruct EmployeeTypeenum (FullTime, PartTime, Contractor)- Each type has different payment calculation
- Implement
calculate_salary()method
Example:
let emp = Employee::new("John", EmployeeType::FullTime(50000));
println!("Salary: {}", emp.monthly_salary());
Hints:
- Store employee type as enum with data
- Match on type for salary calculation
- Consider adding more methods
Solution
#[derive(Debug)]
enum EmployeeType {
FullTime { annual_salary: u32 },
PartTime { hourly_rate: u32, hours_per_week: u32 },
Contractor { hourly_rate: u32 },
}
#[derive(Debug)]
struct Employee {
name: String,
id: u32,
employee_type: EmployeeType,
}
impl Employee {
fn new(name: &str, id: u32, employee_type: EmployeeType) -> Self {
Employee {
name: name.to_string(),
id,
employee_type,
}
}
fn monthly_salary(&self) -> u32 {
match &self.employee_type {
EmployeeType::FullTime { annual_salary } => annual_salary / 12,
EmployeeType::PartTime { hourly_rate, hours_per_week } => {
hourly_rate * hours_per_week * 4
}
EmployeeType::Contractor { hourly_rate } => {
// Contractors bill variable hours, return base rate
*hourly_rate * 160 // Assume 160 hours/month
}
}
}
fn employment_status(&self) -> &str {
match &self.employee_type {
EmployeeType::FullTime { .. } => "Full-Time",
EmployeeType::PartTime { .. } => "Part-Time",
EmployeeType::Contractor { .. } => "Contractor",
}
}
fn give_raise(&mut self, percentage: f64) {
match &mut self.employee_type {
EmployeeType::FullTime { annual_salary } => {
*annual_salary = (*annual_salary as f64 * (1.0 + percentage / 100.0)) as u32;
}
EmployeeType::PartTime { hourly_rate, .. } => {
*hourly_rate = (*hourly_rate as f64 * (1.0 + percentage / 100.0)) as u32;
}
EmployeeType::Contractor { hourly_rate } => {
*hourly_rate = (*hourly_rate as f64 * (1.0 + percentage / 100.0)) as u32;
}
}
}
}
fn main() {
let mut employees = vec![
Employee::new("Alice", 1, EmployeeType::FullTime { annual_salary: 60000 }),
Employee::new("Bob", 2, EmployeeType::PartTime {
hourly_rate: 20,
hours_per_week: 20,
}),
Employee::new("Carol", 3, EmployeeType::Contractor { hourly_rate: 50 }),
];
for emp in &employees {
println!("{} ({}): ${}/month",
emp.name, emp.employment_status(), emp.monthly_salary());
}
// Give Alice a 10% raise
employees[0].give_raise(10.0);
println!("\nAfter Alice's raise:");
println!("{}: ${}/month", employees[0].name, employees[0].monthly_salary());
}
Learning Points:
- Modeling domain entities
- Enum variants with named fields
- Mutable references to enum data
- Business logic in methods
Exercise 11: Builder Pattern
Difficulty: Medium
Problem: Implement the builder pattern for a User struct.
Requirements:
- Create
UserBuilderstruct - Required fields: username
- Optional fields: email, age, location
- Chain methods together
build()returnsResult<User, String>
Example:
let user = User::builder()
.username("john_doe")
.email("[email protected]")
.age(30)
.build()?;
Hints:
- Builder methods take
selfand returnSelf - Use
Optionfor optional fields - Validate in
build()method
Solution
#[derive(Debug)]
struct User {
username: String,
email: Option<String>,
age: Option<u32>,
location: Option<String>,
}
struct UserBuilder {
username: Option<String>,
email: Option<String>,
age: Option<u32>,
location: Option<String>,
}
impl User {
fn builder() -> UserBuilder {
UserBuilder {
username: None,
email: None,
age: None,
location: None,
}
}
}
impl UserBuilder {
fn username(mut self, username: &str) -> Self {
self.username = Some(username.to_string());
self
}
fn email(mut self, email: &str) -> Self {
self.email = Some(email.to_string());
self
}
fn age(mut self, age: u32) -> Self {
self.age = Some(age);
self
}
fn location(mut self, location: &str) -> Self {
self.location = Some(location.to_string());
self
}
fn build(self) -> Result<User, String> {
let username = self.username
.ok_or("Username is required")?;
// Validate username
if username.len() < 3 {
return Err("Username must be at least 3 characters".to_string());
}
// Validate age if provided
if let Some(age) = self.age {
if age < 13 {
return Err("User must be at least 13 years old".to_string());
}
}
Ok(User {
username,
email: self.email,
age: self.age,
location: self.location,
})
}
}
fn main() {
let user1 = User::builder()
.username("john_doe")
.email("[email protected]")
.age(30)
.location("New York")
.build();
match user1 {
Ok(user) => println!("Created user: {:?}", user),
Err(e) => println!("Error: {}", e),
}
// Missing username
let user2 = User::builder()
.email("[email protected]")
.build();
match user2 {
Ok(user) => println!("Created user: {:?}", user),
Err(e) => println!("Error: {}", e),
}
// Invalid age
let user3 = User::builder()
.username("kid")
.age(10)
.build();
match user3 {
Ok(user) => println!("Created user: {:?}", user),
Err(e) => println!("Error: {}", e),
}
}
Learning Points:
- Builder pattern in Rust
- Method chaining
- Validation in constructors
- Using
Resultfor fallible construction
Exercise 12: Binary Tree
Difficulty: Hard
Problem: Implement a simple binary search tree.
Requirements:
- Create
TreeNodewith left and right children - Implement
insert()method - Implement
contains()method - Implement in-order traversal
Example:
let mut tree = BinaryTree::new();
tree.insert(5);
tree.insert(3);
tree.insert(7);
println!("Contains 3: {}", tree.contains(3));
Hints:
- Use
Option<Box<TreeNode>> - Recursive insertion and search
- Compare values for BST property
Solution
#[derive(Debug)]
struct TreeNode<T: Ord> {
value: T,
left: Option<Box<TreeNode<T>>>,
right: Option<Box<TreeNode<T>>>,
}
#[derive(Debug)]
struct BinaryTree<T: Ord> {
root: Option<Box<TreeNode<T>>>,
}
impl<T: Ord> BinaryTree<T> {
fn new() -> Self {
BinaryTree { root: None }
}
fn insert(&mut self, value: T) {
self.root = Self::insert_node(self.root.take(), value);
}
fn insert_node(node: Option<Box<TreeNode<T>>>, value: T) -> Option<Box<TreeNode<T>>> {
match node {
None => Some(Box::new(TreeNode {
value,
left: None,
right: None,
})),
Some(mut n) => {
if value < n.value {
n.left = Self::insert_node(n.left.take(), value);
} else if value > n.value {
n.right = Self::insert_node(n.right.take(), value);
}
// If equal, don't insert (no duplicates)
Some(n)
}
}
}
fn contains(&self, value: &T) -> bool {
Self::contains_node(&self.root, value)
}
fn contains_node(node: &Option<Box<TreeNode<T>>>, value: &T) -> bool {
match node {
None => false,
Some(n) => {
if value == &n.value {
true
} else if value < &n.value {
Self::contains_node(&n.left, value)
} else {
Self::contains_node(&n.right, value)
}
}
}
}
fn inorder(&self) -> Vec<&T> {
let mut result = Vec::new();
Self::inorder_node(&self.root, &mut result);
result
}
fn inorder_node<'a>(node: &'a Option<Box<TreeNode<T>>>, result: &mut Vec<&'a T>) {
if let Some(n) = node {
Self::inorder_node(&n.left, result);
result.push(&n.value);
Self::inorder_node(&n.right, result);
}
}
}
fn main() {
let mut tree = BinaryTree::new();
let values = vec![5, 3, 7, 1, 4, 6, 9];
for val in values {
tree.insert(val);
}
println!("Tree: {:?}", tree);
println!("Inorder traversal: {:?}", tree.inorder());
println!("Contains 4: {}", tree.contains(&4));
println!("Contains 8: {}", tree.contains(&8));
}
Learning Points:
- Recursive data structures
Boxfor owned heap data- Recursive algorithms
- Binary search tree properties
- Generic types with trait bounds (
Ord)
Exercise 13: Card Deck
Difficulty: Medium
Problem: Model a deck of playing cards.
Requirements:
- Create
SuitandRankenums - Create
Cardstruct - Create
Deckstruct with shuffle and deal methods - Implement comparison for cards
Example:
let mut deck = Deck::new();
deck.shuffle();
let card = deck.deal();
Hints:
- Use
randcrate for shuffling - Derive necessary traits
- Implement ordering for poker rules
Solution
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Suit {
Hearts,
Diamonds,
Clubs,
Spades,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum Rank {
Two = 2,
Three,
Four,
Five,
Six,
Seven,
Eight,
Nine,
Ten,
Jack,
Queen,
King,
Ace,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Card {
suit: Suit,
rank: Rank,
}
impl Card {
fn new(suit: Suit, rank: Rank) -> Self {
Card { suit, rank }
}
}
impl std::fmt::Display for Card {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:?} of {:?}", self.rank, self.suit)
}
}
#[derive(Debug)]
struct Deck {
cards: Vec<Card>,
}
impl Deck {
fn new() -> Self {
let mut cards = Vec::new();
for &suit in &[Suit::Hearts, Suit::Diamonds, Suit::Clubs, Suit::Spades] {
for &rank in &[
Rank::Two, Rank::Three, Rank::Four, Rank::Five,
Rank::Six, Rank::Seven, Rank::Eight, Rank::Nine,
Rank::Ten, Rank::Jack, Rank::Queen, Rank::King, Rank::Ace,
] {
cards.push(Card::new(suit, rank));
}
}
Deck { cards }
}
fn shuffle(&mut self) {
use rand::seq::SliceRandom;
let mut rng = rand::thread_rng();
self.cards.shuffle(&mut rng);
}
fn deal(&mut self) -> Option<Card> {
self.cards.pop()
}
fn cards_remaining(&self) -> usize {
self.cards.len()
}
}
fn main() {
let mut deck = Deck::new();
println!("New deck has {} cards", deck.cards_remaining());
deck.shuffle();
println!("Shuffled!");
println!("\nDealing 5 cards:");
for _ in 0..5 {
if let Some(card) = deck.deal() {
println!(" {}", card);
}
}
println!("\nCards remaining: {}", deck.cards_remaining());
}
Note: Add rand = "0.8" to your Cargo.toml.
Learning Points:
- Modeling real-world entities
- Multiple related enums
- Implementing traits (
Display,PartialOrd) - Using external crates for functionality
Exercise 14: Message Types
Difficulty: Medium
Problem: Create a messaging system with different message types.
Requirements:
Messageenum with Text, Image, Video variants- Each variant stores relevant data
- Implement
size()method - Implement
preview()method
Example:
let msg = Message::Text("Hello".to_string());
println!("Size: {} bytes", msg.size());
Hints:
- Store metadata for each type
- Size calculation differs by type
- Preview should be concise
Solution
#[derive(Debug, Clone)]
enum Message {
Text {
content: String,
},
Image {
url: String,
width: u32,
height: u32,
size_bytes: usize,
},
Video {
url: String,
duration_seconds: u32,
size_bytes: usize,
},
}
impl Message {
fn size(&self) -> usize {
match self {
Message::Text { content } => content.len(),
Message::Image { size_bytes, .. } => *size_bytes,
Message::Video { size_bytes, .. } => *size_bytes,
}
}
fn preview(&self) -> String {
match self {
Message::Text { content } => {
if content.len() <= 50 {
content.clone()
} else {
format!("{}...", &content[..47])
}
}
Message::Image { url, width, height, .. } => {
format!("Image: {} ({}x{})", url, width, height)
}
Message::Video { url, duration_seconds, .. } => {
let minutes = duration_seconds / 60;
let seconds = duration_seconds % 60;
format!("Video: {} ({}:{:02})", url, minutes, seconds)
}
}
}
fn message_type(&self) -> &str {
match self {
Message::Text { .. } => "text",
Message::Image { .. } => "image",
Message::Video { .. } => "video",
}
}
}
fn main() {
let messages = vec![
Message::Text {
content: "Hello, World!".to_string(),
},
Message::Image {
url: "photo.jpg".to_string(),
width: 1920,
height: 1080,
size_bytes: 524288,
},
Message::Video {
url: "video.mp4".to_string(),
duration_seconds: 125,
size_bytes: 10485760,
},
Message::Text {
content: "This is a very long message that should be truncated in the preview".to_string(),
},
];
for msg in messages {
println!("{} - {} bytes", msg.preview(), msg.size());
println!(" Type: {}\n", msg.message_type());
}
}
Learning Points:
- Enum variants with struct-like syntax
- Pattern matching on complex variants
- String truncation and formatting
- Domain modeling
Exercise 15: Type-Safe IDs
Difficulty: Medium
Problem: Create type-safe wrapper types for different ID types.
Requirements:
- Create
UserId,ProductId,OrderIdnewtypes - Prevent mixing different ID types
- Implement
DisplayandFrom<u32> - Add validation
Example:
let user_id = UserId::new(1);
let product_id = ProductId::new(100);
// user_id == product_id // Compile error!
Hints:
- Use tuple structs for newtypes
- Implement traits for convenience
- Can't accidentally mix different ID types
Solution
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct UserId(u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct ProductId(u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct OrderId(u32);
impl UserId {
fn new(id: u32) -> Result<Self, String> {
if id == 0 {
Err("User ID cannot be zero".to_string())
} else {
Ok(UserId(id))
}
}
fn value(&self) -> u32 {
self.0
}
}
impl ProductId {
fn new(id: u32) -> Result<Self, String> {
if id == 0 {
Err("Product ID cannot be zero".to_string())
} else {
Ok(ProductId(id))
}
}
fn value(&self) -> u32 {
self.0
}
}
impl OrderId {
fn new(id: u32) -> Result<Self, String> {
if id == 0 {
Err("Order ID cannot be zero".to_string())
} else {
Ok(OrderId(id))
}
}
fn value(&self) -> u32 {
self.0
}
}
impl fmt::Display for UserId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "User#{}", self.0)
}
}
impl fmt::Display for ProductId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Product#{}", self.0)
}
}
impl fmt::Display for OrderId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Order#{}", self.0)
}
}
struct Order {
id: OrderId,
user_id: UserId,
product_ids: Vec<ProductId>,
}
impl Order {
fn new(id: OrderId, user_id: UserId, product_ids: Vec<ProductId>) -> Self {
Order {
id,
user_id,
product_ids,
}
}
fn print_summary(&self) {
println!("Order: {}", self.id);
println!(" User: {}", self.user_id);
println!(" Products:");
for product_id in &self.product_ids {
println!(" {}", product_id);
}
}
}
fn main() {
let user_id = UserId::new(42).unwrap();
let product1 = ProductId::new(100).unwrap();
let product2 = ProductId::new(101).unwrap();
let order_id = OrderId::new(1000).unwrap();
let order = Order::new(order_id, user_id, vec![product1, product2]);
order.print_summary();
// This would be a compile error - can't mix ID types:
// let order_bad = Order::new(user_id, order_id, vec![]);
// Validation works:
match UserId::new(0) {
Ok(id) => println!("Created: {}", id),
Err(e) => println!("Error: {}", e),
}
}
Learning Points:
- Newtype pattern for type safety
- Preventing type confusion at compile time
- Implementing traits for custom types
- Validation in constructors
- Using
Hashfor use in collections
Tips for Success
- Start Simple - Begin with basic structs and enums before complex patterns
- Think About Ownership - Consider whether methods need
&self,&mut self, orself - Use Derive - Automatically implement common traits with
#[derive(...)] - Pattern Match - Enums and pattern matching go hand in hand
- Model Your Domain - Use types to represent your problem domain accurately
Next Steps
- Learn about trait objects and dynamic dispatch
- Explore advanced pattern matching
- Study smart pointers (
Box,Rc,Arc,RefCell) - Practice with iterator exercises
- Build larger projects combining these concepts