Rust Series : Borrow Checker Part 5 | as Design Partner – Concurrency, Async, and Mastery

The final frontier: mastering lifetimes across threads, async boundaries, and complex systems.

Core Concepts and Internal Features

Understanding Arc (Atomically Reference Counted)

Arc is Rust’s thread-safe reference counting smart pointer. Unlike Rc which is single-threaded, Arc uses atomic operations to manage reference counts safely across threads.

Key Features:

  • Atomic Reference Counting: Uses atomic integers to track references
  • Send + Sync: Can be safely sent between threads and shared across threads
  • Immutable by Default: Provides shared ownership but not shared mutability
  • Clone Semantics: Arc::clone() creates new references, not data copies

Internal Mechanism:

// Conceptual representation
struct Arc<T> {
    data: *const T,           // Pointer to data
    ref_count: AtomicUsize,   // Thread-safe reference counter
}

Understanding Mutex (Mutual Exclusion)

Mutex provides mutual exclusion for shared data, ensuring only one thread can access the data at a time.

Key Features:

  • Blocking Lock: lock() blocks until lock is acquired
  • RAII Lock Guard: Returns a guard that automatically unlocks when dropped
  • Poisoning: If a thread panics while holding the lock, the mutex becomes “poisoned”
  • Interior Mutability: Allows mutation of data inside an immutable reference

How Interior Mutability Works:

Interior mutability is a design pattern in Rust that allows you to mutate data even when you have an immutable reference to it. This seemingly breaks Rust’s borrowing rules, but it’s actually safe because the mutation is controlled by runtime checks or synchronization primitives.

The Core Concept:

// Normal Rust: Can't mutate through immutable reference
let data = vec![1, 2, 3];
let immutable_ref = &data;
// immutable_ref.push(4); // ERROR: Cannot mutate through immutable reference

// Interior Mutability: Can mutate through "immutable" reference
use std::cell::RefCell;
let data = RefCell::new(vec![1, 2, 3]);
let immutable_ref = &data;  // This reference is immutable
immutable_ref.borrow_mut().push(4); // But we can still mutate the contents!

Key Types That Provide Interior Mutability:

  1. UnsafeCell – The foundation of all interior mutability
  2. Cell – For Copy types, provides get/set methods
  3. RefCell – For any type, provides runtime borrow checking
  4. Mutex – For thread-safe interior mutability
  5. RwLock – For multiple readers or single writer scenarios
  6. AtomicT – For atomic operations on primitive types

UnsafeCell: The Foundation

use std::cell::UnsafeCell;

// UnsafeCell is the only type that allows mutation through shared references
struct MyCell<T> {
    inner: UnsafeCell<T>,
}

impl<T> MyCell<T> {
    fn new(value: T) -> Self {
        Self {
            inner: UnsafeCell::new(value),
        }
    }

    fn get(&self) -> *mut T {
        self.inner.get() // Returns raw pointer to inner data
    }

    // UNSAFE: Caller must ensure no aliasing violations
    unsafe fn set(&self, value: T) {
        *self.get() = value;
    }
}

RefCell: Runtime Borrow Checking

use std::cell::RefCell;

let data = RefCell::new(vec![1, 2, 3]);

// These work because RefCell tracks borrows at runtime
{
    let borrowed = data.borrow(); // Immutable borrow
    println!("Length: {}", borrowed.len());
} // Borrow ends here

{
    let mut borrowed = data.borrow_mut(); // Mutable borrow
    borrowed.push(4);
} // Mutable borrow ends here

// This would panic at runtime:
// let borrow1 = data.borrow();
// let borrow2 = data.borrow_mut(); // PANIC: Already borrowed!

How RefCell Implements Interior Mutability:

// Simplified RefCell implementation
struct RefCell<T> {
    value: UnsafeCell<T>,
    borrow_count: Cell<isize>, // Positive = shared borrows, -1 = exclusive borrow
}

impl<T> RefCell<T> {
    fn borrow(&self) -> Ref<T> {
        let count = self.borrow_count.get();
        if count < 0 {
            panic!("Already mutably borrowed!");
        }
        self.borrow_count.set(count + 1);
        // Return a guard that decrements count on drop
        Ref { /* ... */ }
    }

    fn borrow_mut(&self) -> RefMut<T> {
        let count = self.borrow_count.get();
        if count != 0 {
            panic!("Already borrowed!");
        }
        self.borrow_count.set(-1);
        // Return a guard that resets count on drop
        RefMut { /* ... */ }
    }
}

Mutex: Thread-Safe Interior Mutability

Mutex extends interior mutability to multi-threaded scenarios:

use std::sync::Mutex;

let data = Mutex::new(vec![1, 2, 3]);

// From any thread, through an immutable reference:
let guard = data.lock().unwrap(); // Returns MutexGuard<Vec<i32>>
// guard.push(4); // Can mutate through the guard

How Mutex Achieves Thread-Safe Interior Mutability:

// Conceptual Mutex implementation
struct Mutex<T> {
    data: UnsafeCell<T>,          // The actual data
    lock: sys::Mutex,             // Platform-specific mutex
    poisoned: AtomicBool,         // Poison flag for panic handling
}

impl<T> Mutex<T> {
    fn lock(&self) -> LockResult<MutexGuard<T>> {
        // 1. Acquire the OS-level lock (blocks if necessary)
        self.lock.lock();

        // 2. Check if poisoned (previous thread panicked while holding lock)
        if self.poisoned.load(Ordering::Relaxed) {
            return Err(PoisonError::new(/* ... */));
        }

        // 3. Return guard that provides access to data
        Ok(MutexGuard {
            data: &self.data,
            lock: &self.lock,
            poisoned: &self.poisoned,
        })
    }
}

// The guard provides the actual mutation capability
struct MutexGuard<'a, T> {
    data: &'a UnsafeCell<T>,
    lock: &'a sys::Mutex,
    poisoned: &'a AtomicBool,
}

impl<T> Deref for MutexGuard<'_, T> {
    type Target = T;

    fn deref(&self) -> &T {
        unsafe { &*self.data.get() }
    }
}

impl<T> DerefMut for MutexGuard<'_, T> {
    fn deref_mut(&mut self) -> &mut T {
        unsafe { &mut *self.data.get() }
    }
}

impl<T> Drop for MutexGuard<'_, T> {
    fn drop(&mut self) {
        // Handle potential panic during drop
        if std::thread::panicking() {
            self.poisoned.store(true, Ordering::Relaxed);
        }
        // Release the lock
        self.lock.unlock();
    }
}

Why Interior Mutability is Safe:

  1. Compile-Time + Runtime Checks: The borrow checker ensures structural safety, while interior mutability types add runtime checks
  2. RAII Guards: Lock guards automatically release locks/borrows when dropped
  3. Poison Handling: Mutexes become “poisoned” if a thread panics while holding the lock
  4. Type System Integration: The type system ensures you can only access data through the proper guards

Real-World Example in the Tutorial:

let counter = Arc::new(Mutex::new(0));
//                     ^^^^^^^^^^^^
//                     Interior mutability: can mutate through shared reference

let counter_clone = Arc::clone(&counter);
thread::spawn(move || {
    let mut num = counter_clone.lock().unwrap();
    //            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    //            Gets MutexGuard<i32> which implements DerefMut
    *num += 1; // Actually mutating through the guard
});

Interior Mutability vs Normal Mutability:

// Normal mutability: Exclusive access guaranteed at compile time
let mut data = vec![1, 2, 3];
data.push(4); // Direct mutation

// Interior mutability: Shared access with controlled mutation
let data = Mutex::new(vec![1, 2, 3]);
data.lock().unwrap().push(4); // Mutation through runtime-checked guard

Internal Mechanism:

// Conceptual representation
struct Mutex<T> {
    data: UnsafeCell<T>,      // Interior mutability primitive
    lock: sys::Mutex,         // Platform-specific mutex (pthread_mutex_t on Unix)
    poisoned: AtomicBool,     // Poison flag for panic safety
}

Understanding Channels (mpsc::channel)

mpsc stands for “Multiple Producer, Single Consumer” – a channel implementation for thread communication.

Key Features:

  • Ownership Transfer: Sending moves ownership to the receiver
  • Synchronous by Default: Sender blocks if channel buffer is full
  • Automatic Cleanup: Channel closes when all senders are dropped
  • Type Safety: Compile-time guarantees about message types

Internal Mechanism:

// Conceptual representation
struct Sender<T> {
    shared: Arc<Shared<T>>,
}

struct Receiver<T> {
    shared: Arc<Shared<T>>,
}

struct Shared<T> {
    queue: Mutex<VecDeque<T>>,
    sender_count: AtomicUsize,
}

Understanding Thread Spawning

thread::spawn creates a new OS thread and executes a closure on it.

Key Features:

  • Move Semantics: Often requires move closure to transfer ownership
  • Send Bound: Closure must be Send to move between threads
  • Join Handle: Returns a handle to wait for thread completion
  • Panic Isolation: Thread panics don’t affect other threads

Understanding Send and Sync Traits

Send: Types that can be transferred between threads safely
Sync: Types that can be shared between threads safely (via &T)

Key Rules:

  • T: Send means T can be moved to another thread
  • T: Sync means &T can be shared between threads
  • Arc<T> is Send + Sync if T: Send + Sync
  • Mutex<T> is Send + Sync if T: Send

The Complete Tutorial Code

fn main() {
    println!("=== Thread Safety with Ownership ===");
    demonstrate_thread_safety();

    println!("n=== Async Lifetime Management ===");
    demonstrate_async_concepts();

    println!("n=== Advanced Techniques ===");
    demonstrate_advanced_techniques();

    println!("n=== Mastery Checklist ===");
    demonstrate_mastery_patterns();
}

// THREAD SAFETY: Send and Sync traits in action
fn demonstrate_thread_safety() {
    use std::sync::{Arc, Mutex};
    use std::thread;

    println!("=== Arc + Mutex: Shared Mutable State ===");

    // CONCEPT: Arc for shared ownership, Mutex for safe mutation
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for i in 0..3 {
        let counter_clone = Arc::clone(&counter);

        let handle = thread::spawn(move || {
            // CONCEPT: Move ownership into thread
            let mut num = counter_clone.lock().unwrap();
            *num += i;
            println!("Thread {} updated counter", i);
        });

        handles.push(handle);
    }

    // Wait for all threads to complete
    for handle in handles {
        handle.join().unwrap();
    }

    println!("Final counter value: {}", *counter.lock().unwrap());

    println!("n=== Channel Communication ===");

    use std::sync::mpsc;

    // CONCEPT: Channels transfer ownership between threads
    let (sender, receiver) = mpsc::channel();

    // Spawn producer thread
    let producer = thread::spawn(move || {
        for i in 0..3 {
            let message = format!("Message {}", i);
            sender.send(message).unwrap(); // Ownership transferred
            println!("Sent message {}", i);
        }
        // Sender dropped here, closing channel
    });

    // Consume messages in main thread
    let consumer = thread::spawn(move || {
        while let Ok(message) = receiver.recv() {
            println!("Received: {}", message);
        }
    });

    producer.join().unwrap();
    consumer.join().unwrap();

    println!("n=== Scoped Threads with Borrowing ===");

    let data = vec![1, 2, 3, 4, 5];

    thread::scope(|s| {
        // CONCEPT: Scoped threads can borrow from parent scope
        s.spawn(|| {
            // Can borrow data because scope guarantees lifetime
            let sum: i32 = data.iter().sum();
            println!("Sum from thread: {}", sum);
        });

        s.spawn(|| {
            let max = data.iter().max().unwrap_or(&0);
            println!("Max from thread: {}", max);
        });

        // All scoped threads complete before scope ends
    });

    println!("Data still available: {:?}", data);
}

// ASYNC: Lifetime management across async boundaries
fn demonstrate_async_concepts() {
    println!("=== Async Lifetime Challenges (Conceptual) ===");

    // CONCEPT: Async functions have special lifetime requirements
    // Real async code would need tokio/async-std runtime

    struct AsyncProcessor {
        config: String,
    }

    impl AsyncProcessor {
        fn new(config: String) -> Self {
            Self { config }
        }

        // PATTERN: Async methods must own their data or use 'static
        async fn process_owned(&self, data: String) -> String {
            // CONCEPT: self and data must live for entire async lifetime
            format!("{}: {}", self.config, data)
        }

        // PATTERN: Use Arc for shared data across async boundaries
        fn create_shared(config: String) -> Arc<Self> {
            Arc::new(Self::new(config))
        }
    }

    // SIMULATION: What async code looks like
    let processor = AsyncProcessor::new("ASYNC".to_string());

    // In real async code, this would be:
    // let result = processor.process_owned("test data".to_string()).await;
    let simulated_result = "ASYNC: test data"; // Simulated async result
    println!("Async result: {}", simulated_result);

    println!("n=== Async-Safe Patterns ===");

    use std::sync::Arc;

    // PATTERN 1: Arc for shared async state
    let shared_processor = AsyncProcessor::create_shared("SHARED".to_string());
    let task_processor = Arc::clone(&shared_processor);

    // In real async:
    // let future = async move {
    //     task_processor.process_owned("async data".to_string()).await
    // };

    println!("Shared processor config: {}", task_processor.config);

    // PATTERN 2: Send + Sync bounds for async functions
    fn async_compatible<T>(_data: T) 
    where 
        T: Send + Sync + 'static 
    {
        // CONCEPT: Async functions often require Send + Sync + 'static
        println!("Data is async-compatible");
    }

    let safe_data = Arc::new("This is Send + Sync + 'static".to_string());
    async_compatible(safe_data);

    // PATTERN 3: Owned data for async tasks
    let owned_data = "Owned by the async task".to_string();
    // In real async: let task = async move { process(owned_data).await };
    println!("Owned data ready for async: {}", owned_data);
}

// ADVANCED TECHNIQUES: Expert-level patterns
fn demonstrate_advanced_techniques() {
    println!("=== Phantom Types for Lifetime Safety ===");

    use std::marker::PhantomData;

    // CONCEPT: Phantom types can enforce lifetime relationships
    struct Database<'conn> {
        connection_id: u32,
        _phantom: PhantomData<&'conn ()>,
    }

    struct Query<'db, 'conn> {
        sql: &'db str,
        database: &'db Database<'conn>,
    }

    impl<'conn> Database<'conn> {
        fn new(id: u32) -> Self {
            Self {
                connection_id: id,
                _phantom: PhantomData,
            }
        }

        fn query<'db>(&'db self, sql: &'db str) -> Query<'db, 'conn> {
            Query {
                sql,
                database: self,
            }
        }
    }

    impl<'db, 'conn> Query<'db, 'conn> {
        fn execute(&self) -> String {
            format!("Executing '{}' on DB {}", self.sql, self.database.connection_id)
        }
    }

    let db = Database::new(1);
    let query = db.query("SELECT * FROM users");
    let result = query.execute();
    println!("Query result: {}", result);

    println!("n=== Higher-Ranked Trait Bounds (HRTB) ===");

    // CONCEPT: Functions that work with any lifetime
    fn process_with_closure<F>(data: &str, processor: F) -> String
    where
        F: for<'a> Fn(&'a str) -> String,
    {
        // CONCEPT: 'for<'a>' means "for any lifetime 'a'"
        processor(data)
    }

    let result = process_with_closure("input", |s| s.to_uppercase());
    println!("HRTB result: {}", result);

    println!("n=== Self-Referential Structs (Advanced) ===");

    // CONCEPT: Sometimes you need structs that reference themselves
    // This is advanced and usually requires unsafe or special libraries

    struct SelfReferential {
        data: String,
        // In real code, this would be more complex
        reference_count: usize,
    }

    impl SelfReferential {
        fn new(data: String) -> Self {
            Self {
                data,
                reference_count: 0,
            }
        }

        fn get_reference(&mut self) -> &str {
            self.reference_count += 1;
            &self.data
        }

        fn reference_count(&self) -> usize {
            self.reference_count
        }
    }

    let mut self_ref = SelfReferential::new("Self-referential data".to_string());
    let reference = self_ref.get_reference();
    println!("Reference: {}", reference);
    println!("Reference count: {}", self_ref.reference_count());
}

// MASTERY: Patterns that show true understanding
fn demonstrate_mastery_patterns() {
    println!("=== Mastery Pattern 1: Zero-Cost Abstractions ===");

    // CONCEPT: Abstractions that compile away to optimal code
    struct Validator<T> {
        value: T,
    }

    impl<T> Validator<T> {
        fn new(value: T) -> Self {
            Self { value }
        }

        fn validate<F>(self, validator: F) -> Result<T, &'static str>
        where
            F: FnOnce(&T) -> bool,
        {
            if validator(&self.value) {
                Ok(self.value) // No runtime cost for wrapper
            } else {
                Err("Validation failed")
            }
        }
    }

    let validated = Validator::new(42)
        .validate(|&x| x > 0)
        .unwrap();

    println!("Validated value: {}", validated);

    println!("n=== Mastery Pattern 2: Type-State Programming ===");

    // CONCEPT: Use types to enforce state transitions
    struct Unconnected;
    struct Connected;

    struct NetworkClient<State> {
        address: String,
        _state: PhantomData<State>,
    }

    impl NetworkClient<Unconnected> {
        fn new(address: String) -> Self {
            Self {
                address,
                _state: PhantomData,
            }
        }

        fn connect(self) -> NetworkClient<Connected> {
            println!("Connecting to {}", self.address);
            NetworkClient {
                address: self.address,
                _state: PhantomData,
            }
        }
    }

    impl NetworkClient<Connected> {
        fn send_data(&self, data: &str) {
            println!("Sending '{}' to {}", data, self.address);
        }

        fn disconnect(self) -> NetworkClient<Unconnected> {
            println!("Disconnecting from {}", self.address);
            NetworkClient {
                address: self.address,
                _state: PhantomData,
            }
        }
    }

    let client = NetworkClient::new("127.0.0.1:8080".to_string());
    let connected = client.connect();
    connected.send_data("Hello, Server!");
    let _disconnected = connected.disconnect();

    println!("n=== Mastery Pattern 3: Lifetime-Parameterized Collections ===");

    // CONCEPT: Collections that can hold references with specific lifetimes
    struct BorrowedVec<'a, T> {
        items: Vec<&'a T>,
    }

    impl<'a, T> BorrowedVec<'a, T> {
        fn new() -> Self {
            Self { items: Vec::new() }
        }

        fn push(&mut self, item: &'a T) {
            self.items.push(item);
        }

        fn get(&self, index: usize) -> Option<&'a T> {
            self.items.get(index).copied()
        }

        fn len(&self) -> usize {
            self.items.len()
        }
    }

    let data1 = "First string".to_string();
    let data2 = "Second string".to_string();

    let mut borrowed_vec = BorrowedVec::new();
    borrowed_vec.push(&data1);
    borrowed_vec.push(&data2);

    println!("Borrowed vec length: {}", borrowed_vec.len());
    if let Some(first) = borrowed_vec.get(0) {
        println!("First item: {}", first);
    }
}

Advanced Concept Deep Dive

1. Phantom Types and Lifetimes

PhantomData is a zero-sized type that tells the compiler about relationships that aren’t explicitly stored in the struct:

use std::marker::PhantomData;

struct Database<'conn> {
    connection_id: u32,
    _phantom: PhantomData<&'conn ()>,  // Tells compiler about 'conn lifetime
}

Why This Works:

  • The phantom type parameter connects the struct to a lifetime
  • Compiler treats the struct as if it contains a reference with that lifetime
  • Zero runtime cost – compiles away completely
  • Prevents use-after-free bugs at compile time

2. Higher-Ranked Trait Bounds (HRTB)

for<‘a> syntax means “for any lifetime ‘a'”:

fn process_with_closure<F>(data: &str, processor: F) -> String
where
    F: for<'a> Fn(&'a str) -> String,  // Works with any lifetime

Real-World Use Cases:

  • Iterator adapters that work with any lifetime
  • Callback functions that must work with borrowed data
  • Generic functions that don’t know the specific lifetime

3. Scoped Threads

thread::scope creates a scope where spawned threads can borrow from the parent:

thread::scope(|s| {
    s.spawn(|| {
        // Can borrow from parent scope
        let sum: i32 = data.iter().sum();
    });
    // All threads guaranteed to complete before scope ends
});

Why This Is Safe:

  • Scope ensures all spawned threads complete before returning
  • Borrowed data is guaranteed to outlive the thread scope
  • No need for Arc/Mutex for read-only access

4. Type-State Programming

Use the type system to enforce state transitions:

struct NetworkClient<State> {
    address: String,
    _state: PhantomData<State>,
}

impl NetworkClient<Unconnected> {
    fn connect(self) -> NetworkClient<Connected> { /* ... */ }
}

impl NetworkClient<Connected> {
    fn send_data(&self, data: &str) { /* ... */ }
}

Benefits:

  • Impossible to send data on unconnected client
  • State transitions enforced at compile time
  • Clear API that prevents misuse
  • Zero runtime cost

5. Async Lifetime Requirements

Async functions have special lifetime requirements:

async fn process_data(&self, data: String) -> String {
    // Both &self and data must live for the entire async lifetime
    format!("{}: {}", self.config, data)
}

Common Patterns:

  • Use Arc for shared state across async boundaries
  • Prefer owned data (String) over borrowed data (&str)
  • Add Send + Sync + 'static bounds for async-compatible types

Memory Safety Guarantees

Arc + Mutex Pattern

let counter = Arc::new(Mutex::new(0));
let counter_clone = Arc::clone(&counter);

thread::spawn(move || {
    let mut num = counter_clone.lock().unwrap();
    *num += 1;
});

Safety Guarantees:

  • No Data Races: Mutex ensures exclusive access
  • No Use-After-Free: Arc keeps data alive until last reference drops
  • No Double-Free: Arc uses atomic reference counting
  • Deadlock Prevention: Lock guards automatically unlock on drop

Channel Communication

let (sender, receiver) = mpsc::channel();

thread::spawn(move || {
    sender.send(message).unwrap(); // Ownership transferred
});

let received = receiver.recv().unwrap(); // Ownership received

Safety Guarantees:

  • No Shared Mutable State: Data ownership is transferred
  • No Data Races: Only one thread owns the data at a time
  • Type Safety: Compile-time guarantees about message types
  • Resource Cleanup: Channel automatically closes when senders drop

Performance Characteristics

Zero-Cost Abstractions

Many Rust patterns compile to the same assembly as hand-optimized C:

  • Arc: Compiles to atomic increment/decrement operations
  • Mutex: Compiles to platform-specific mutex operations
  • Channels: Compile to efficient lock-free queues when possible
  • Phantom Types: Completely eliminated at compile time

Memory Layout

// These have identical memory layout:
struct Simple { data: i32 }
struct WithPhantom<'a> { data: i32, _phantom: PhantomData<&'a ()> }

The phantom type adds zero bytes to the struct – it’s purely a compile-time construct.

Common Pitfalls and Solutions

1. Async Lifetime Issues

Problem: Borrowing across await points

async fn bad_example(&self) -> String {
    let borrowed = &self.data;
    some_async_function().await; // Error: borrowed doesn't live long enough
    borrowed.to_string()
}

Solution: Use owned data or Arc

async fn good_example(&self) -> String {
    let owned = self.data.clone();
    some_async_function().await;
    owned.to_string()
}

2. Mutex Deadlocks

Problem: Multiple locks in different orders

// Thread 1: locks A then B
// Thread 2: locks B then A
// Result: Deadlock!

Solution: Always acquire locks in the same order

// Both threads lock in A -> B order
let lock_a = mutex_a.lock().unwrap();
let lock_b = mutex_b.lock().unwrap();

3. Channel Lifetime Issues

Problem: Trying to send borrowed data

let data = String::from("hello");
sender.send(&data).unwrap(); // Error: borrowed data can't be sent

Solution: Send owned data or use Arc

let data = String::from("hello");
sender.send(data).unwrap(); // OK: ownership transferred

Mastery Checklist

You’ve mastered Rust concurrency and lifetimes when you can:

✅ Design thread-safe APIs using Arc, Mutex, and channels

✅ Reason about Send/Sync bounds and their implications

✅ Use scoped threads for efficient borrowing across threads

✅ Handle async lifetime requirements with appropriate patterns

✅ Apply phantom types for compile-time safety guarantees

✅ Implement type-state programming for API design

✅ Use HRTB for flexible generic functions

✅ Debug lifetime errors systematically and efficiently

✅ Choose appropriate concurrency primitives for each use case

✅ Write zero-cost abstractions that maintain performance

The borrow checker is no longer your enemy – it’s your most trusted design partner! Every “fight” with the borrow checker is actually a conversation about better design. Listen to what it’s telling you, and your code will be safer, faster, and more elegant.

Next Steps

  1. Practice these patterns in real projects
  2. Study the Rust standard library source code
  3. Contribute to open-source Rust projects
  4. Explore advanced topics like proc macros and unsafe code
  5. Teach others – teaching solidifies understanding

Happy coding! 🦀

About Me

https://www.linkedin.com/in/kumarabhishek21/

Similar Posts