Activity 22 - Generics
Overview
In this activity, you'll practice writing generic functions and structs, working with trait bounds, and understanding how Rust's type system enables flexible, reusable code. All exercises can be completed in the Rust Playground.
Part 1: Fix the Trait Bounds (Warm-up)
Learning goal: Understanding which trait bounds are needed for different operations
Important Note: Some traits need to be imported! Copy this template to start each problem in the Rust Playground
#![allow(unused)] fn main() { // Common trait imports you might need: #![allow(unused_imports)] // to ignore unused import warnings use std::fmt::{Debug, Display}; use std::cmp::{PartialOrd, PartialEq, Eq, Ord}; use std::ops::{Add, Sub, Mul, Div}; // Note: Copy, Clone are automatically imported }
Instructions For each problem copy the snippet into Rust Playground and work on it until it compiles. The compiler errors will be very helpful!
Problem 1.1: Printing with Debug
Fix this function by adding the correct trait bound(s):
// Fix this function so it compiles fn print_twice<T>(value: T) { println!("{:?}", value); } fn main() { print_twice(42); print_twice("hello"); }
Problem 1.2: Comparison
Fix this function by adding the correct trait bound:
// Fix this function so it compiles fn is_greater<T>(a: T, b: T) -> bool { a > b } fn main() { println!("{}", is_greater(5, 3)); println!("{}", is_greater(2.5, 7.8)); }
Problem 1.3: Multiple Uses with Display
Fix this function by adding the correct trait bounds (you'll need multiple!):
// Fix this function so it compiles fn compare_and_print<T>(a: T, b: T) { if a > b { println!("{:?} is greater", a); } else if a == b { println!("They are equal!"); } else { println!("{:?} is greater", b); } } fn main() { compare_and_print(5, 3); compare_and_print(2.5, 7.8); }
Part 2: Build a Generic Container
Learning goal: Creating and implementing methods for generic structs
Implement a generic Pair struct that holds two values of the same type.
#[derive(Debug)] struct Pair<T> { first: T second: T } // 1. Implement a constructor method `new` impl<T> Pair<T> { // fn new(first: T, second: T) -> Pair<T> } // 2. Implement a method `swap` that returns a new Pair with values swapped // You can do this without adding a trait bound! impl<T> Pair<T> { // fn swap(self) -> Pair<T> } // 3. Implement a method `are_equal` that returns true if first == second // (You'll need a trait bound on this impl block!) impl<T: ???> Pair<T> { // fn are_equal(&self) -> bool } // Work until this compiles! fn main() { let pair = Pair::new(5, 10); println!("Original: {:?}", pair); let swapped = pair.swap(); println!("Swapped: {:?}", swapped); println!("Are equal? {}", pair.are_equal()); let equal_pair = Pair::new(7, 7); println!("Are equal? {}", equal_pair.are_equal()); }
Bonus: Add a method max that returns a reference to the larger of the two values. What trait bound do you need?
Part 3: Two Different Types
Learning goal: Working with multiple type parameters
Sometimes you want to store two values of DIFFERENT types. Implement this:
#[derive(Debug)] struct Pair<T, U> { first: T second: U } // Implement methods: impl<T, U> Pair<T, U> { // 1. Constructor fn new(first: T, second: U) -> Pair<T, U> { // Your code here } // 2. Get first value as reference fn get_first(&self) -> &T { // Your code here } // 3. Get second value as reference fn get_second(&self) -> &U { // Your code here } // 4. Swap the values (notice the return type!) fn swap(self) -> Pair<U, T> { // Your code here } } fn main() { let mixed = Pair::new(42, "hello"); println!("First: {}, Second: {}", mixed.get_first(), mixed.get_second()); let swapped = mixed.swap(); println!("Swapped: {:?}", swapped); // Now swapped is Pair<&str, i32> instead of Pair<i32, &str>! }
Part 4: Challenge - Option Revisited
Now that you understand generics, try to implement your own version of Option<T> from scratch (call it Maybe<T>). Implement is_some(), is_none(), unwrap(), and unwrap_or() methods.