Skip to content

Instantly share code, notes, and snippets.

@rust-play
Created June 22, 2025 13:34
Show Gist options
  • Save rust-play/c16f0bc7e1a53cbcd44e58c891f6a4fd to your computer and use it in GitHub Desktop.
Save rust-play/c16f0bc7e1a53cbcd44e58c891f6a4fd to your computer and use it in GitHub Desktop.
Code shared from the Rust Playground
fn main() {
// COMPLETE PROGRAM: Demonstrating scope trees and lifetime relationships
println!("=== Scope Tree Visualization ===");
scope_tree_example();
println!("\n=== Lifetime Elision Rules ===");
elision_examples();
println!("\n=== When Elision Fails ===");
explicit_lifetime_examples();
println!("\n=== Multiple Lifetime Parameters ===");
multiple_lifetime_examples();
}
fn scope_tree_example() {
// CONCEPT: Nested scopes create a tree structure
// Each scope level is like a generation in a family tree
// ROOT SCOPE: Grandparent level
let grandparent = String::from("Ancient wisdom passed down");
println!("Grandparent created: {}", grandparent);
{ // CHILD SCOPE 1: Parent level
// CONCEPT: Child scopes can borrow from parent scopes
let parent = &grandparent; // Borrows from grandparent
println!("Parent borrows: {}", parent);
{ // GRANDCHILD SCOPE: Child level
// CONCEPT: Children can borrow from their parents
// This creates a chain: grandchild -> parent -> grandparent
let child = &parent; // Borrows from parent (who borrowed from grandparent)
println!("Child borrows: {}", child);
// CONCEPT: All three generations coexist safely
// The borrow checker ensures no conflicts
println!("Three generations: '{}', '{}', '{}'",
grandparent, parent, child);
// CONCEPT: The rule - children cannot outlive their parents
// `child` must be dropped before `parent`
// `parent` must be dropped before `grandparent`
} // Child scope ends - `child` is dropped here
// CONCEPT: Parent can still be used after child is gone
println!("Parent survives child: {}", parent);
} // Parent scope ends - `parent` is dropped here
// CONCEPT: Grandparent survives all descendants
println!("Grandparent survives all: {}", grandparent);
// CONCEPT: Why this works
// The compiler tracks the lifetime hierarchy:
// - grandparent: lives for entire function
// - parent: lives for middle scope, borrows from grandparent
// - child: lives for inner scope, borrows from parent
// No reference outlives its source!
}
// CONCEPT: Lifetime elision - the compiler's auto-complete for lifetimes
// Rule 1: Each parameter gets its own lifetime
fn process_single_input(input: &str) -> String {
// WHAT YOU WRITE: No explicit lifetimes
// WHAT COMPILER SEES: fn process_single_input<'a>(input: &'a str) -> String
// CONCEPT: Since we're returning owned data (String),
// no lifetime relationship exists between input and output
input.to_uppercase()
}
// Rule 2: Single input reference → output gets same lifetime
fn get_first_word(text: &str) -> &str {
// WHAT YOU WRITE: No explicit lifetimes
// WHAT COMPILER INFERS: fn get_first_word<'a>(text: &'a str) -> &'a str
// CONCEPT: The output is a slice of the input
// So the output must live as long as the input
text.split_whitespace().next().unwrap_or("")
}
// Rule 3: Method with &self → output gets self's lifetime
struct TextAnalyzer {
content: String,
}
impl TextAnalyzer {
fn new(content: String) -> Self {
Self { content }
}
// WHAT YOU WRITE: No explicit lifetimes
fn get_content(&self) -> &str {
// WHAT COMPILER INFERS: fn get_content<'a>(&'a self) -> &'a str
// CONCEPT: The returned slice borrows from self
// The slice can't outlive the TextAnalyzer instance
&self.content
}
fn get_word_count(&self) -> usize {
// CONCEPT: No lifetime issues here - returning owned data
self.content.split_whitespace().count()
}
// CONCEPT: Multiple borrows from self are fine
fn get_summary(&self) -> (usize, &str) {
// WHAT COMPILER INFERS: fn get_summary<'a>(&'a self) -> (usize, &'a str)
let word_count = self.get_word_count();
let first_word = self.get_content().split_whitespace().next().unwrap_or("");
(word_count, first_word)
}
}
fn elision_examples() {
// CONCEPT: Testing elision rules in practice
let data = "Hello world from Rust";
// Rule 1: Single input, owned output
let processed = process_single_input(data);
println!("Processed (owned): {}", processed);
// Rule 2: Single input, borrowed output
let first_word = get_first_word(data);
println!("First word (borrowed): {}", first_word);
// Rule 3: Method with &self
let analyzer = TextAnalyzer::new(data.to_string());
let content = analyzer.get_content();
let (count, first) = analyzer.get_summary();
println!("Content: {}", content);
println!("Summary: {} words, starts with '{}'", count, first);
// CONCEPT: All borrows are valid because analyzer outlives all references
}
// CONCEPT: When elision fails - compiler needs explicit help
fn explicit_lifetime_examples() {
let text1 = "Hello world";
let text2 = "Rust programming";
// CONCEPT: When multiple inputs exist, compiler can't guess
// which input the output should be tied to
let longer = choose_longer_explicit(&text1, &text2);
println!("Longer text: {}", longer);
// CONCEPT: Different lifetime requirements
let first_part = get_first_part_explicit(&text1, &text2);
println!("First part of first input: {}", first_part);
}
// CONCEPT: Explicit lifetime when compiler can't infer
fn choose_longer_explicit<'a>(s1: &'a str, s2: &'a str) -> &'a str {
// CONCEPT: Both inputs must live as long as the output
// The 'a lifetime ties all three together
// This means: "s1 and s2 must both live at least as long as the returned reference"
if s1.len() > s2.len() {
s1 // Could return either input
} else {
s2 // So both must live long enough
}
}
// CONCEPT: Different lifetimes when only one input matters
fn get_first_part_explicit<'a, 'b>(primary: &'a str, _secondary: &'b str) -> &'a str {
// CONCEPT: Output only depends on 'primary'
// 'secondary' can have a completely different lifetime
// This is more flexible than forcing both to have the same lifetime
&primary[0..primary.len().min(5)] // Return first 5 chars of primary
}
fn multiple_lifetime_examples() {
// CONCEPT: Testing functions with multiple lifetime parameters
let long_lived = String::from("This is a long-lived string");
{ // Shorter scope
let short_lived = String::from("Short");
// CONCEPT: Both inputs available, function works
let result1 = choose_longer_explicit(&long_lived, &short_lived);
println!("Result 1: {}", result1);
// CONCEPT: Only primary input matters for this function
let result2 = get_first_part_explicit(&long_lived, &short_lived);
println!("Result 2: {}", result2);
// CONCEPT: result2 can outlive short_lived because it only
// depends on long_lived (due to different lifetime parameters)
} // short_lived dropped here
// This would work for result2 but not result1:
// println!("After scope: {}", result2); // Would be OK
// println!("After scope: {}", result1); // Would be ERROR
}
// CONCEPT: Complex lifetime relationships
struct DataProcessor<'data> {
// CONCEPT: Struct that holds a reference to external data
// The struct cannot outlive the data it references
source: &'data str,
processed_count: usize,
}
impl<'data> DataProcessor<'data> {
fn new(source: &'data str) -> Self {
// CONCEPT: The processor borrows the source data
// It doesn't own the data, just processes it
Self {
source,
processed_count: 0,
}
}
fn process_chunk(&mut self, start: usize, len: usize) -> Option<&'data str> {
// CONCEPT: Return a slice that lives as long as the original source
// The lifetime 'data ensures the slice is valid
if start + len <= self.source.len() {
self.processed_count += 1;
Some(&self.source[start..start + len])
} else {
None
}
}
fn get_stats(&self) -> (usize, usize) {
// CONCEPT: Returning owned data - no lifetime constraints
(self.source.len(), self.processed_count)
}
}
// CONCEPT: Advanced lifetime demonstration
fn demonstrate_processor() {
println!("\n=== Advanced Lifetime Relationships ===");
let source_data = "The quick brown fox jumps over the lazy dog";
// CONCEPT: Create processor that borrows source_data
let mut processor = DataProcessor::new(source_data);
// CONCEPT: Process chunks - returned slices borrow from original data
if let Some(chunk1) = processor.process_chunk(0, 9) {
println!("Chunk 1: '{}'", chunk1);
}
if let Some(chunk2) = processor.process_chunk(10, 5) {
println!("Chunk 2: '{}'", chunk2);
}
// CONCEPT: Get statistics - owned data, no lifetime constraints
let (total_len, processed_count) = processor.get_stats();
println!("Processed {} chunks from {} characters", processed_count, total_len);
// CONCEPT: All chunks and processor must be dropped before source_data
// The borrow checker enforces this automatically
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_elision_rules() {
// CONCEPT: Testing that elision works as expected
let input = "test input";
// Rule 1: owned output
let owned = process_single_input(input);
assert_eq!(owned, "TEST INPUT");
// Rule 2: borrowed output tied to input
let borrowed = get_first_word(input);
assert_eq!(borrowed, "test");
// Rule 3: method with &self
let analyzer = TextAnalyzer::new(input.to_string());
let content = analyzer.get_content();
assert_eq!(content, input);
}
#[test]
fn test_explicit_lifetimes() {
// CONCEPT: Testing explicit lifetime annotations
let s1 = "short";
let s2 = "much longer string";
let result = choose_longer_explicit(s1, s2);
assert_eq!(result, s2); // s2 is longer
let first_part = get_first_part_explicit(s1, s2);
assert_eq!(first_part, "short"); // First 5 chars of s1
}
#[test]
fn test_data_processor() {
// CONCEPT: Testing struct with lifetime parameters
let data = "Hello Rust World";
let mut processor = DataProcessor::new(data);
let chunk = processor.process_chunk(0, 5).unwrap();
assert_eq!(chunk, "Hello");
let (len, count) = processor.get_stats();
assert_eq!(len, 16);
assert_eq!(count, 1);
}
}
// Run the demonstrations
fn run_demonstrations() {
demonstrate_processor();
}
// CONCEPT: Entry point that ties everything together
// This shows the complete flow of lifetime concepts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment