You've already forked rust-tutor
generic types, traits and lifetimes
- get claude to create rustlings style exercises based on chapter 10 of the book - update CLAUDE.md to prevent it from producing incorrect exercises - complete exercises/01-generic-cache-system
This commit is contained in:
6
exercises/01-generic-cache-system/Cargo.toml
Normal file
6
exercises/01-generic-cache-system/Cargo.toml
Normal file
@@ -0,0 +1,6 @@
|
||||
[package]
|
||||
name = "generic_function_toolkit"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
15
exercises/01-generic-cache-system/README.md
Normal file
15
exercises/01-generic-cache-system/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Generic Cache System
|
||||
|
||||
Build a simple key-value cache that can store any types. You'll write generic functions that manage cached data, demonstrating how generics let you write one implementation that works with strings, numbers, or any other type.
|
||||
|
||||
## What you'll practice
|
||||
|
||||
- Writing generic functions with multiple type parameters `<K, V>`
|
||||
- Using trait bounds to constrain what types can be used
|
||||
- Working with `HashMap<K, V>` and generic collections
|
||||
- Combining `Option<T>` and `Result<T, E>` with generics
|
||||
|
||||
## Further information
|
||||
|
||||
- [Generic Data Types](https://doc.rust-lang.org/book/ch10-01-syntax.html)
|
||||
- [Using Trait Bounds](https://doc.rust-lang.org/book/ch10-02-traits.html#using-trait-bounds-to-conditionally-implement-methods)
|
||||
112
exercises/01-generic-cache-system/src/main.rs
Normal file
112
exercises/01-generic-cache-system/src/main.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use std::collections::HashMap;
|
||||
use std::hash::Hash;
|
||||
|
||||
fn cache_get<K: Eq + Hash, V>(map: &HashMap<K, V>, key: K) -> Option<&V> {
|
||||
for (k, v) in map.iter() {
|
||||
if *k == key {
|
||||
return Some(&v);
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
fn cache_insert<K: Eq + Hash, V: Copy>(cache: &mut HashMap<K, V>, key: K, value: V) -> Option<V> {
|
||||
for (k, v) in cache.iter() {
|
||||
let return_value = *v;
|
||||
if *k == key {
|
||||
//update value
|
||||
cache.insert(key, value);
|
||||
}
|
||||
//"there was an existing record with this key which we wrote over"
|
||||
//"and the key we wrote over was return_value"
|
||||
return Some(return_value);
|
||||
}
|
||||
// insert new value
|
||||
cache.insert(key, value);
|
||||
// "there wasnt an existing record with this key"
|
||||
return None;
|
||||
}
|
||||
|
||||
fn cache_compute_if_absent<K: Eq + Hash, V, F>(cache: &mut HashMap<K, V>, key: K, compute: F) -> &V
|
||||
where
|
||||
F: Fn() -> V,
|
||||
{
|
||||
cache.entry(key).or_insert_with(compute)
|
||||
}
|
||||
|
||||
fn batch_lookup<'a, K: Eq + Hash, V>(cache: &'a HashMap<K, V>, keys: &[K]) -> Vec<&'a V> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
for key in keys {
|
||||
if let Some(value) = cache.get(key) {
|
||||
result.push(value);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("let's go!");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_cache_get_found() {
|
||||
let mut cache = HashMap::new();
|
||||
cache.insert("key1", 42);
|
||||
assert_eq!(cache_get(&cache, &"key1"), Some(&42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_get_missing() {
|
||||
let cache: HashMap<&str, i32> = HashMap::new();
|
||||
assert_eq!(cache_get(&cache, &"missing"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_insert_new() {
|
||||
let mut cache = HashMap::new();
|
||||
assert_eq!(cache_insert(&mut cache, "new", 100), None);
|
||||
assert_eq!(cache.get("new"), Some(&100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_insert_existing() {
|
||||
let mut cache = HashMap::new();
|
||||
cache.insert("key", 50);
|
||||
assert_eq!(cache_insert(&mut cache, "key", 75), Some(50));
|
||||
assert_eq!(cache.get("key"), Some(&75));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_if_absent_missing() {
|
||||
let mut cache = HashMap::new();
|
||||
let result = cache_compute_if_absent(&mut cache, "compute", || "computed".to_string());
|
||||
assert_eq!(result, &"computed".to_string());
|
||||
assert_eq!(cache.get("compute"), Some(&"computed".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_if_absent_exists() {
|
||||
let mut cache = HashMap::new();
|
||||
cache.insert("existing", "original".to_string());
|
||||
let result =
|
||||
cache_compute_if_absent(&mut cache, "existing", || "should not run".to_string());
|
||||
assert_eq!(result, &"original".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_batch_lookup() {
|
||||
let mut cache = HashMap::new();
|
||||
cache.insert(1, "one");
|
||||
cache.insert(3, "three");
|
||||
cache.insert(5, "five");
|
||||
|
||||
let keys = [1, 2, 3, 4, 5];
|
||||
let results = batch_lookup(&cache, &keys);
|
||||
assert_eq!(results, vec![&"one", &"three", &"five"]);
|
||||
}
|
||||
}
|
||||
6
exercises/02-generic-structs/Cargo.toml
Normal file
6
exercises/02-generic-structs/Cargo.toml
Normal file
@@ -0,0 +1,6 @@
|
||||
[package]
|
||||
name = "generic_structs"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
15
exercises/02-generic-structs/README.md
Normal file
15
exercises/02-generic-structs/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Generic Message Queue
|
||||
|
||||
Build a message queue system that can handle different types of messages. You'll create generic structs and enums that can queue up emails, notifications, tasks, or any other message type without code duplication.
|
||||
|
||||
## What you'll practice
|
||||
|
||||
- Defining generic structs that hold collections of any type
|
||||
- Creating generic enums for different message states
|
||||
- Writing implementation blocks with generic type parameters
|
||||
- Using associated functions vs instance methods with generics
|
||||
|
||||
## Further information
|
||||
|
||||
- [Generic Data Types](https://doc.rust-lang.org/book/ch10-01-syntax.html)
|
||||
- [Method Definitions](https://doc.rust-lang.org/book/ch05-03-method-syntax.html)
|
||||
102
exercises/02-generic-structs/src/main.rs
Normal file
102
exercises/02-generic-structs/src/main.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
// TODO: Make this struct generic over message type T
|
||||
// It should hold a queue of messages and track total processed count
|
||||
struct MessageQueue {
|
||||
queue: VecDeque<String>,
|
||||
processed_count: usize,
|
||||
}
|
||||
|
||||
// TODO: Make this enum generic over T for message data and E for error data
|
||||
// Messages can be Pending, Processing, or Failed with error info
|
||||
enum MessageStatus {
|
||||
Pending(String),
|
||||
Processing(String),
|
||||
Failed(String, String), // message, error
|
||||
}
|
||||
|
||||
// TODO: Create a generic struct for message metadata
|
||||
// Should store the message of type T and a priority level (u8)
|
||||
struct PriorityMessage {
|
||||
content: String,
|
||||
priority: u8,
|
||||
}
|
||||
|
||||
// TODO: Implement methods for MessageQueue<T>
|
||||
impl MessageQueue<String> {
|
||||
// Create new empty queue
|
||||
fn new() -> Self {
|
||||
todo!("Create new message queue")
|
||||
}
|
||||
|
||||
// Add message to back of queue
|
||||
fn enqueue(&mut self, message: String) {
|
||||
todo!("Add message to queue")
|
||||
}
|
||||
|
||||
// Remove and return next message from front, increment processed count
|
||||
fn dequeue(&mut self) -> Option<String> {
|
||||
todo!("Remove next message")
|
||||
}
|
||||
|
||||
// Get current queue length
|
||||
fn len(&self) -> usize {
|
||||
todo!("Return queue length")
|
||||
}
|
||||
|
||||
// Get total number of processed messages
|
||||
fn total_processed(&self) -> usize {
|
||||
todo!("Return processed count")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_queue_creation() {
|
||||
let queue: MessageQueue<String> = MessageQueue::new();
|
||||
assert_eq!(queue.len(), 0);
|
||||
assert_eq!(queue.total_processed(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enqueue_dequeue() {
|
||||
let mut queue = MessageQueue::new();
|
||||
queue.enqueue("First message".to_string());
|
||||
queue.enqueue("Second message".to_string());
|
||||
|
||||
assert_eq!(queue.len(), 2);
|
||||
assert_eq!(queue.dequeue(), Some("First message".to_string()));
|
||||
assert_eq!(queue.len(), 1);
|
||||
assert_eq!(queue.total_processed(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_status_variants() {
|
||||
let pending: MessageStatus<String, String> = MessageStatus::Pending("Task 1".to_string());
|
||||
let failed: MessageStatus<String, String> = MessageStatus::Failed("Task 2".to_string(), "Connection timeout".to_string());
|
||||
|
||||
match pending {
|
||||
MessageStatus::Pending(msg) => assert_eq!(msg, "Task 1"),
|
||||
_ => panic!("Should be Pending"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_priority_message() {
|
||||
let high_priority: PriorityMessage<String> = PriorityMessage {
|
||||
content: "Urgent task".to_string(),
|
||||
priority: 1,
|
||||
};
|
||||
|
||||
let low_priority: PriorityMessage<i32> = PriorityMessage {
|
||||
content: 42,
|
||||
priority: 5,
|
||||
};
|
||||
|
||||
assert_eq!(high_priority.priority, 1);
|
||||
assert_eq!(low_priority.content, 42);
|
||||
}
|
||||
}
|
||||
6
exercises/03-basic-traits/Cargo.toml
Normal file
6
exercises/03-basic-traits/Cargo.toml
Normal file
@@ -0,0 +1,6 @@
|
||||
[package]
|
||||
name = "basic_traits"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
15
exercises/03-basic-traits/README.md
Normal file
15
exercises/03-basic-traits/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Configuration System Traits
|
||||
|
||||
Build a configuration system where different components (database, cache, logger) can be configured uniformly. You'll define traits that allow different types to be configured, validated, and reset in a standard way.
|
||||
|
||||
## What you'll practice
|
||||
|
||||
- Defining traits that establish contracts between types
|
||||
- Implementing the same trait for different struct types
|
||||
- Using traits as function parameters with `impl Trait` syntax
|
||||
- Creating functions that work with any type implementing specific traits
|
||||
|
||||
## Further information
|
||||
|
||||
- [Traits: Defining Shared Behavior](https://doc.rust-lang.org/book/ch10-02-traits.html)
|
||||
- [Traits as Parameters](https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters)
|
||||
119
exercises/03-basic-traits/src/main.rs
Normal file
119
exercises/03-basic-traits/src/main.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
// TODO: Define a trait called `Configurable` with these methods:
|
||||
// - configure(&mut self, setting: &str, value: &str) -> Result<(), String>
|
||||
// - is_valid(&self) -> bool
|
||||
// - reset(&mut self)
|
||||
trait Configurable {
|
||||
// Add your methods here
|
||||
}
|
||||
|
||||
struct DatabaseConfig {
|
||||
host: String,
|
||||
port: u16,
|
||||
max_connections: u32,
|
||||
}
|
||||
|
||||
struct CacheConfig {
|
||||
size_mb: u32,
|
||||
ttl_seconds: u64,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
struct LoggerConfig {
|
||||
level: String,
|
||||
output_file: Option<String>,
|
||||
timestamp_format: String,
|
||||
}
|
||||
|
||||
// TODO: Implement Configurable for DatabaseConfig
|
||||
// configure should handle: "host", "port", "max_connections"
|
||||
// is_valid should return true if host is not empty and port > 0
|
||||
// reset should set host="localhost", port=5432, max_connections=10
|
||||
|
||||
// TODO: Implement Configurable for CacheConfig
|
||||
// configure should handle: "size_mb", "ttl_seconds", "enabled"
|
||||
// is_valid should return true if size_mb > 0 and ttl_seconds > 0
|
||||
// reset should set size_mb=100, ttl_seconds=300, enabled=true
|
||||
|
||||
// TODO: Implement Configurable for LoggerConfig
|
||||
// configure should handle: "level", "output_file", "timestamp_format"
|
||||
// is_valid should return true if level is one of: "debug", "info", "warn", "error"
|
||||
// reset should set level="info", output_file=None, timestamp_format="%Y-%m-%d %H:%M:%S"
|
||||
|
||||
// TODO: Write a function `apply_config` that takes any Configurable and a list of (key, value) pairs
|
||||
// It should try to configure each setting and return a Vec of error messages for failed configurations
|
||||
fn apply_config(config: &mut impl Configurable, settings: &[(&str, &str)]) -> Vec<String> {
|
||||
todo!("Apply configuration settings")
|
||||
}
|
||||
|
||||
// TODO: Write a function `validate_and_reset_if_invalid` that takes any Configurable
|
||||
// If the config is invalid, reset it and return true. If valid, return false.
|
||||
fn validate_and_reset_if_invalid(config: &mut impl Configurable) -> bool {
|
||||
todo!("Validate config and reset if invalid")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_database_config() {
|
||||
let mut db = DatabaseConfig {
|
||||
host: "".to_string(),
|
||||
port: 0,
|
||||
max_connections: 0,
|
||||
};
|
||||
|
||||
assert!(!db.is_valid());
|
||||
db.reset();
|
||||
assert!(db.is_valid());
|
||||
assert_eq!(db.host, "localhost");
|
||||
assert_eq!(db.port, 5432);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_configure() {
|
||||
let mut cache = CacheConfig {
|
||||
size_mb: 0,
|
||||
ttl_seconds: 0,
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
cache.reset();
|
||||
assert!(cache.is_valid());
|
||||
|
||||
assert!(cache.configure("size_mb", "200").is_ok());
|
||||
assert_eq!(cache.size_mb, 200);
|
||||
|
||||
assert!(cache.configure("invalid_key", "value").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_logger_validation() {
|
||||
let mut logger = LoggerConfig {
|
||||
level: "invalid".to_string(),
|
||||
output_file: None,
|
||||
timestamp_format: "".to_string(),
|
||||
};
|
||||
|
||||
assert!(!logger.is_valid());
|
||||
assert!(validate_and_reset_if_invalid(&mut logger));
|
||||
assert!(logger.is_valid());
|
||||
assert_eq!(logger.level, "info");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_config() {
|
||||
let mut db = DatabaseConfig {
|
||||
host: "".to_string(),
|
||||
port: 0,
|
||||
max_connections: 0,
|
||||
};
|
||||
|
||||
let settings = [("host", "example.com"), ("port", "3306"), ("invalid", "value")];
|
||||
let errors = apply_config(&mut db, &settings);
|
||||
|
||||
assert_eq!(errors.len(), 1); // Only "invalid" should fail
|
||||
assert_eq!(db.host, "example.com");
|
||||
assert_eq!(db.port, 3306);
|
||||
}
|
||||
}
|
||||
6
exercises/04-generic-methods/Cargo.toml
Normal file
6
exercises/04-generic-methods/Cargo.toml
Normal file
@@ -0,0 +1,6 @@
|
||||
[package]
|
||||
name = "generic_methods"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
15
exercises/04-generic-methods/README.md
Normal file
15
exercises/04-generic-methods/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Generic Methods & Implementations
|
||||
|
||||
Build a generic data transformer that can convert, filter, and manipulate collections of any type. You'll practice writing methods on generic structs and using multiple type parameters in implementation blocks.
|
||||
|
||||
## What you'll practice
|
||||
|
||||
- Writing methods for generic structs with `impl<T>` blocks
|
||||
- Using multiple type parameters in methods like `<T, U>`
|
||||
- Associated functions vs instance methods on generic types
|
||||
- Method chaining with generic return types
|
||||
|
||||
## Further information
|
||||
|
||||
- [Method Syntax](https://doc.rust-lang.org/book/ch05-03-method-syntax.html)
|
||||
- [Generic Data Types](https://doc.rust-lang.org/book/ch10-01-syntax.html)
|
||||
122
exercises/04-generic-methods/src/main.rs
Normal file
122
exercises/04-generic-methods/src/main.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
// TODO: Make this struct generic over type T
|
||||
// It should store a collection of items and provide transformation methods
|
||||
struct DataTransformer {
|
||||
items: Vec<i32>,
|
||||
}
|
||||
|
||||
// TODO: Implement methods for DataTransformer<T>
|
||||
impl DataTransformer<i32> {
|
||||
// TODO: Associated function to create new transformer with empty collection
|
||||
fn new() -> Self {
|
||||
todo!("Create new DataTransformer")
|
||||
}
|
||||
|
||||
// TODO: Associated function to create transformer from existing vector
|
||||
fn from_vec(items: Vec<i32>) -> Self {
|
||||
todo!("Create DataTransformer from vector")
|
||||
}
|
||||
|
||||
// TODO: Method to add an item to the collection
|
||||
fn add(&mut self, item: i32) {
|
||||
todo!("Add item to collection")
|
||||
}
|
||||
|
||||
// TODO: Method to get length of collection
|
||||
fn len(&self) -> usize {
|
||||
todo!("Return length")
|
||||
}
|
||||
|
||||
// TODO: Method to transform each item using a closure and return new DataTransformer
|
||||
// This should work: transformer.map(|x| x * 2)
|
||||
fn map<U, F>(&self, f: F) -> DataTransformer<U>
|
||||
where
|
||||
F: Fn(&i32) -> U,
|
||||
{
|
||||
todo!("Transform each item")
|
||||
}
|
||||
|
||||
// TODO: Method to filter items using a predicate and return new DataTransformer with same type
|
||||
fn filter<F>(&self, predicate: F) -> DataTransformer<i32>
|
||||
where
|
||||
F: Fn(&i32) -> bool,
|
||||
{
|
||||
todo!("Filter items")
|
||||
}
|
||||
|
||||
// TODO: Method to find first item matching predicate
|
||||
fn find<F>(&self, predicate: F) -> Option<&i32>
|
||||
where
|
||||
F: Fn(&i32) -> bool,
|
||||
{
|
||||
todo!("Find first matching item")
|
||||
}
|
||||
|
||||
// TODO: Method to collect all items into a vector
|
||||
fn collect(self) -> Vec<i32> {
|
||||
todo!("Collect into vector")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_creation_and_basic_ops() {
|
||||
let mut transformer: DataTransformer<i32> = DataTransformer::new();
|
||||
assert_eq!(transformer.len(), 0);
|
||||
|
||||
transformer.add(10);
|
||||
transformer.add(20);
|
||||
assert_eq!(transformer.len(), 2);
|
||||
|
||||
let from_vec: DataTransformer<String> = DataTransformer::from_vec(vec!["hello".to_string(), "world".to_string()]);
|
||||
assert_eq!(from_vec.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_map_transformation() {
|
||||
let transformer = DataTransformer::from_vec(vec![1, 2, 3, 4]);
|
||||
let doubled: DataTransformer<i32> = transformer.map(|x| x * 2);
|
||||
assert_eq!(doubled.collect(), vec![2, 4, 6, 8]);
|
||||
|
||||
let strings: DataTransformer<String> = transformer.map(|x| x.to_string());
|
||||
assert_eq!(strings.collect(), vec!["1", "2", "3", "4"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter() {
|
||||
let transformer = DataTransformer::from_vec(vec![1, 2, 3, 4, 5, 6]);
|
||||
let evens = transformer.filter(|x| *x % 2 == 0);
|
||||
assert_eq!(evens.collect(), vec![2, 4, 6]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find() {
|
||||
let transformer = DataTransformer::from_vec(vec![10, 20, 30, 40]);
|
||||
assert_eq!(transformer.find(|x| *x > 25), Some(&30));
|
||||
assert_eq!(transformer.find(|x| *x > 50), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_method_chaining() {
|
||||
let result = DataTransformer::from_vec(vec![1, 2, 3, 4, 5, 6])
|
||||
.filter(|x| *x % 2 == 0) // Keep evens: [2, 4, 6]
|
||||
.map(|x| x * 10) // Multiply by 10: [20, 40, 60]
|
||||
.collect();
|
||||
|
||||
assert_eq!(result, vec![20, 40, 60]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_types() {
|
||||
let string_transformer = DataTransformer::from_vec(vec!["rust".to_string(), "is".to_string(), "awesome".to_string()]);
|
||||
|
||||
let lengths: DataTransformer<usize> = string_transformer.map(|s| s.len());
|
||||
assert_eq!(lengths.collect(), vec![4, 2, 7]);
|
||||
|
||||
let long_words = DataTransformer::from_vec(vec!["a".to_string(), "hello".to_string(), "world".to_string()])
|
||||
.filter(|s| s.len() > 3);
|
||||
assert_eq!(long_words.collect(), vec!["hello".to_string(), "world".to_string()]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user