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:
2026-01-22 11:58:33 +00:00
parent f55039411e
commit 02fbf819e4
14 changed files with 567 additions and 0 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
/rust-book
/rustlings
context.md
ideas.md
**/target/

View File

@@ -35,6 +35,31 @@
**Stretch goal:** [Optional extension]
```
### Exercise Validation (CRITICAL)
**BEFORE providing any exercise, I MUST:**
1. **Mental trace-through** - Mentally implement the solution to verify it's solvable
2. **Signature verification** - Check all function signatures are correct for the requirements
3. **Trait bound validation** - Ensure trait bounds match what the implementation actually needs
4. **Test compatibility** - Verify the test cases will work with correct implementations
5. **Compilation check** - Ensure starter code compiles (even with todo!() placeholders)
### File Structure (REQUIRED - Rustlings Style)
**README.md contains:**
- 2-3 sentence concept explanation (like Rustlings)
- What you'll practice (bullet points)
- Links to relevant documentation
- KEEP IT MINIMAL - no dense walls of text
- NO starter code templates or expected output
**Code files contain:**
- ONLY clean starter code with correct function signatures
- ONLY necessary imports and trait bounds
- ONLY test functions using #[cfg(test)] for verification
- Minimal TODO comments explaining what to implement
- NO explanations or tutorials in comments
**Rustlings principle:** Minimal README, focus on code. Student learns by making tests pass.
### Exercise Types Per Milestone
- 2 general exercises (core programming concepts)
- 1 crypto-specific exercise (domain building)
@@ -76,6 +101,8 @@
## Learning Constraints to Respect
- Student quits when frustrated - keep exercises achievable
- **Incorrect exercise design destroys confidence - ALWAYS validate before providing**
- **Unsolvable exercises are worse than no exercises - verify signatures and bounds**
- Avoid "copy-paste progress" - ensure understanding
- Exercises must be tightly coupled to building (not random drills)
- Prefer modifying existing code over starting from scratch

View File

@@ -0,0 +1,6 @@
[package]
name = "generic_function_toolkit"
version = "0.1.0"
edition = "2024"
[dependencies]

View 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)

View 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"]);
}
}

View File

@@ -0,0 +1,6 @@
[package]
name = "generic_structs"
version = "0.1.0"
edition = "2024"
[dependencies]

View 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)

View 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);
}
}

View File

@@ -0,0 +1,6 @@
[package]
name = "basic_traits"
version = "0.1.0"
edition = "2024"
[dependencies]

View 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)

View 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);
}
}

View File

@@ -0,0 +1,6 @@
[package]
name = "generic_methods"
version = "0.1.0"
edition = "2024"
[dependencies]

View 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)

View 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()]);
}
}