Skip to content

Latest commit

 

History

History
392 lines (294 loc) · 10.7 KB

FUTURE_TRAIT_TUTORIAL.md

File metadata and controls

392 lines (294 loc) · 10.7 KB

The Complete Future Trait Tutorial: From Basics to Advanced Patterns

Table of Contents

  1. Introduction to Asynchronous Programming in Rust
  2. Understanding the Future Trait
  3. Basic Examples: Your First Futures
  4. Implementing Custom Futures
  5. Working with Combinators
  6. Error Handling in Async Code
  7. Real-World Examples
  8. Advanced Patterns and Best Practices
  9. Testing Async Code
  10. Performance Considerations
  11. Common Pitfalls and How to Avoid Them

Introduction to Asynchronous Programming in Rust {#introduction}

Asynchronous programming in Rust allows you to write concurrent code that can handle multiple tasks efficiently without blocking threads. At the heart of Rust's async ecosystem is the Future trait, which represents a computation that will complete at some point in the future.

Key Concepts

  • Futures are lazy: They don't do any work until polled
  • Cooperative multitasking: Tasks yield control voluntarily at .await points
  • Zero-cost abstractions: Async/await compiles to efficient state machines
  • Memory safety: Pin ensures self-referential futures remain valid

Why Use Async Programming?

// Synchronous code - blocks the thread
fn sync_example() {
    let data1 = fetch_data_from_api(); // Blocks for 100ms
    let data2 = fetch_data_from_db();  // Blocks for 200ms
    // Total time: ~300ms
}

// Asynchronous code - concurrent execution
async fn async_example() {
    let future1 = fetch_data_from_api(); // Returns immediately
    let future2 = fetch_data_from_db();  // Returns immediately
    let (data1, data2) = tokio::join!(future1, future2);
    // Total time: ~200ms (max of both operations)
}

Understanding the Future Trait {#understanding-future}

The Future trait is the foundation of async programming in Rust:

pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

pub enum Poll<T> {
    Ready(T),    // The future has completed
    Pending,     // The future is not ready yet
}

Core Components

  1. Output: The type of value the future will produce
  2. poll(): Method called by the executor to advance the future
  3. Pin: Ensures the future doesn't move in memory
  4. Context: Provides access to the Waker for efficient scheduling

Basic Examples: Your First Futures {#basic-examples}

Let's start with simple examples to understand how futures work.

What you'll learn:

  • How to define and use async functions in Rust.
  • The difference between sequential and concurrent execution.
  • Why futures are "lazy" and only do work when awaited.

Example: Basic async/await

// An async function that simulates work by sleeping for a given duration.
async fn simple_async_operation(duration: Duration, message: &str) -> String {
    println!("Starting: {}", message);
    sleep(duration).await; // Non-blocking sleep
    format!("{} completed!", message)
}

// Running multiple async operations sequentially:
let result1 = simple_async_operation(Duration::from_millis(100), "Task 1").await;
let result2 = simple_async_operation(Duration::from_millis(200), "Task 2").await;

// Running them concurrently with tokio::join!:
let (result1, result2) = tokio::join!(
    simple_async_operation(Duration::from_millis(100), "Task 1"),
    simple_async_operation(Duration::from_millis(200), "Task 2")
);

How to run:

cargo run --bin basic_future

Example 2: Understanding Poll States

File: src/examples/poll_states.rs

This example shows how the Poll enum works and when futures return Ready vs Pending.

Example 3: Multiple Futures with join!

File: src/examples/join_example.rs

This example demonstrates concurrent execution of multiple futures.

Implementing Custom Futures {#custom-futures}

What you'll learn:

  • How to implement the Future trait manually.
  • How to use Waker and shared state for efficient async scheduling.
  • The principle of "lazy" futures: work starts only when polled.

Example: Custom Delay Future

// A custom future that completes after a delay.
pub struct DelayFuture {
    shared_state: Arc<Mutex<SharedState>>,
    duration: Duration,
    started: bool,
}

impl Future for DelayFuture {
    type Output = String;
    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let shared_state = self.shared_state.lock().unwrap();
        if shared_state.completed {
            return Poll::Ready(format!("Delay of {:?} completed!", self.duration));
        }
        drop(shared_state);
        self.start_timer(); // Spawns a thread to wait and wake
        let mut shared_state = self.shared_state.lock().unwrap();
        shared_state.waker = Some(cx.waker().clone());
        Poll::Pending
    }
}

How to run:

cargo run --bin custom_delay

Example 5: Custom Join Future

File: src/examples/custom_join.rs

Implementation of a custom combinator that runs multiple futures concurrently.

Working with Combinators {#combinators}

What you'll learn:

  • How to use combinators like map, and_then, join!, and select! to compose async workflows.
  • How to handle errors and transform results in async chains.
  • How to run collections of futures concurrently.

Example: Using map and join! combinators

// Transform the output of a future using map
let result = simulate_database_query("users", Duration::from_millis(100))
    .map(|data| format!("TRANSFORMED: {}", data.to_uppercase()))
    .await;

// Run multiple async operations concurrently with join!
let (api_result, db_result) = tokio::join!(
    simulate_api_call("orders", Duration::from_millis(150), true),
    simulate_database_query("inventory", Duration::from_millis(200))
);

How to run:

cargo run --bin combinators

Error Handling in Async Code {#error-handling}

What you'll learn:

  • How to use Result<T, E> in async functions.
  • How to propagate errors with ? and handle them with pattern matching.
  • How to define and use custom error types with thiserror.

Example: Custom error handling in async Rust

#[derive(Error, Debug, Clone)]
pub enum ApiError {
    #[error("Network error: {message}")]
    NetworkError { message: String },
    #[error("Authentication failed: {reason}")]
    AuthenticationError { reason: String },
    // ... other variants ...
}

async fn simulate_api_request(endpoint: &str, should_succeed: bool) -> Result<String, ApiError> {
    if should_succeed {
        Ok(format!("API response from '{}' endpoint", endpoint))
    } else {
        Err(ApiError::NetworkError { message: "Failed to connect".to_string() })
    }
}

// Handling errors with pattern matching
match simulate_api_request("users", false).await {
    Ok(data) => println!("Success: {}", data),
    Err(ApiError::NetworkError { message }) => println!("Network failed: {}", message),
    Err(e) => println!("Other error: {}", e),
}

How to run:

cargo run --bin error_handling

Real-World Examples {#real-world}

What you'll learn:

  • How to use async HTTP clients (reqwest) and JSON serialization (serde).
  • How to aggregate data from multiple async sources.
  • How to implement caching, rate limiting, and error resilience in real-world async code.

Example: Making an async HTTP request with caching

let client = ApiClient::new("https://jsonplaceholder.typicode.com");
let users = client.get_users().await?;

// ApiClient uses caching and rate limiting internally
let posts = client.get_user_posts(1).await?;

How to run:

cargo run --bin real_world

Advanced Patterns and Best Practices {#advanced-patterns}

Example 9: Advanced Patterns

File: src/examples/advanced_patterns.rs

Advanced techniques including:

  • Custom executors
  • Stream processing
  • Backpressure handling
  • Cancellation-safe futures
  • Performance optimization

Testing Async Code {#testing}

Example 10: Testing Strategies

File: src/tests/async_tests.rs

Comprehensive testing examples:

  • Unit testing async functions
  • Integration testing
  • Mocking async dependencies
  • Testing error conditions
  • Performance testing

Performance Considerations {#performance}

Benchmarking Async Code

  • When to use spawn() vs direct .await
  • Memory allocation patterns
  • CPU vs I/O bound tasks
  • Choosing the right runtime

Common Pitfalls and How to Avoid Them {#pitfalls}

1. Forgetting to .await

// ❌ Wrong - future is created but never executed
async fn wrong_example() {
    fetch_data(); // This does nothing!
}

// ✅ Correct - future is awaited
async fn correct_example() {
    fetch_data().await; // This executes the future
}

2. Blocking in Async Context

// ❌ Wrong - blocks the async runtime
async fn wrong_blocking() {
    std::thread::sleep(Duration::from_secs(1)); // Blocks!
}

// ✅ Correct - uses async sleep
async fn correct_async() {
    tokio::time::sleep(Duration::from_secs(1)).await;
}

3. Send + Sync Issues

// ❌ Wrong - Rc is not Send
async fn wrong_send() {
    let data = Rc::new(42);
    some_async_operation().await;
    println!("{}", data); // Error: Rc held across await
}

// ✅ Correct - Arc is Send + Sync
async fn correct_send() {
    let data = Arc::new(42);
    some_async_operation().await;
    println!("{}", data); // Works!
}

Running the Examples

Each example can be run individually:

# Basic future example
cargo run --bin basic_future

# Custom delay implementation
cargo run --bin custom_delay

# Combinators example
cargo run --bin combinators

# Error handling patterns
cargo run --bin error_handling

# Real-world HTTP client
cargo run --bin real_world

# Advanced patterns
cargo run --bin advanced_patterns

Testing All Examples

Run the complete test suite:

# Run all tests
cargo test

# Run with output
cargo test -- --nocapture

# Run specific test
cargo test test_custom_delay

Next Steps

After completing this tutorial, you should:

  1. Understand the Future trait and its components
  2. Be able to implement custom futures when needed
  3. Know how to use combinators effectively
  4. Handle errors properly in async code
  5. Avoid common pitfalls
  6. Write testable async code

Additional Resources


This tutorial provides hands-on, tested examples for learning the Future trait in Rust. All code examples are verified to compile and run correctly.