Sling Academy
Home/Rust/Method Chaining and Fluent APIs on Rust Structs

Method Chaining and Fluent APIs on Rust Structs

Last updated: January 03, 2025

In software development, method chaining is a design pattern that allows you to call multiple methods on the same object using the same object reference. It improves code readability and fluency, particularly when paired with fluent APIs. Rust, a systems programming language, offers a unique approach to method chaining and fluent APIs due to its compiler checks, type safety, and ownership model. In this article, we'll explore how to employ these techniques on Rust structs, improving code expressiveness without sacrificing safety.

Understanding Method Chaining

Method chaining involves structuring your methods so that they return self, thereby enabling consecutive method calls in a chain. Consider this simple example where method chaining transforms a struct:


struct Meal {
    food: String,
    drink: String,
}

impl Meal {
    fn new() -> Meal {
        Meal {
            food: String::new(),
            drink: String::new(),
        }
    }

    fn food(mut self, food: &str) -> Meal {
        self.food = food.to_string();
        self
    }

    fn drink(mut self, drink: &str) -> Meal {
        self.drink = drink.to_string();
        self
    }

    fn serve(self) {
        println!("Serving a meal with {} and {}.", self.food, self.drink);
    }
}

fn main() {
    Meal::new().food("Pizza").drink("Cola").serve();
}

In the example above, each method returns self, which allows for each method call to be linked or chained together in a fluid way. This makes the code easier to read and understand.

Fluent APIs in Rust

Fluent APIs capitalize on method chaining to create explicit and human-readable structures. They are intuitive and often resemble natural language. In Rust, implementing a fluent API likewise involves returning self in methods, providing a smooth, logical flow of actions on your structs. Let's modify our Meal struct to incorporate more elements with a fluent API:


struct Party {
    title: String,
    location: String,
    date: String,
}

impl Party {
    fn new() -> Party {
        Party {
            title: String::new(),
            location: String::new(),
            date: String::new(),
        }
    }

    fn title(mut self, title: &str) -> Party {
        self.title = title.to_string();
        self
    }

    fn location(mut self, location: &str) -> Party {
        self.location = location.to_string();
        self
    }

    fn date(mut self, date: &str) -> Party {
        self.date = date.to_string();
        self
    }

    fn finalize(self) {
        println!("Finalizing the party: '{}' at {} on {}.",
                 self.title, self.location, self.date);
    }
}

fn main() {
    Party::new().title("Birthday Bash")
               .location("City Park")
               .date("July 20th")
               .finalize();
}

This pattern is especially helpful when multiple attributes are sequentially set or executed. By configuring APIs this way, developers can easily understand the flow of data.

Borrowing vs. Owning in Method Chaining

Rust ensures memory safety through its ownership model. When chaining methods, be mindful of ownership principles to avoid "use after move" errors. If a method signature consumes the self (i.e., self without a reference), ensure subsequent calls don't require access to moved resources. Otherwise, implement the method to take &self or &mut self:


struct Settings {
    volume: u8,
    brightness: u8,
}

impl Settings {
    fn new() -> Settings {
        Settings { volume: 0, brightness: 0 }
    }

    fn volume(&mut self, vol: u8) -> &mut Settings {
        self.volume = vol;
        self
    }

    fn brightness(&mut self, bright: u8) -> &mut Settings {
        self.brightness = bright;
        self
    }

    fn apply(&self) {
        println!("Applying settings: Volume {}%, Brightness {}%.", self.volume, self.brightness);
    }
}

fn main() {
    let mut settings = Settings::new();
    settings.volume(75)
            .brightness(60)
            .apply();
}

Notice the use of &mut Settings as the return type. This allows further modifications and chaining while holding mutable reference, keeping the object alive in the same scope.

Conclusion

Method chaining and fluent APIs enhance the clarity and developer experience by maintaining an easy-to-follow sequence of operations. In relevant cases, Rust’s borrow checker enforces safe resource management, preventing obscure errors. By combining this comfort of fluent interfaces with Rust’s guarantee of safety, developers gain expressively safe, efficient code. Experiment with these patterns to build robust, memorable APIs in your Rust projects.

Next Article: Rust - Unit Structs and Zero-Sized Types for Marker Traits

Previous Article: Working with HashMap and Other Collections Inside Rust Structs

Series: Working with structs in Rust

Rust

You May Also Like

  • E0557 in Rust: Feature Has Been Removed or Is Unavailable in the Stable Channel
  • Network Protocol Handling Concurrency in Rust with async/await
  • Using the anyhow and thiserror Crates for Better Rust Error Tests
  • Rust - Investigating partial moves when pattern matching on vector or HashMap elements
  • Rust - Handling nested or hierarchical HashMaps for complex data relationships
  • Rust - Combining multiple HashMaps by merging keys and values
  • Composing Functionality in Rust Through Multiple Trait Bounds
  • E0437 in Rust: Unexpected `#` in macro invocation or attribute
  • Integrating I/O and Networking in Rust’s Async Concurrency
  • E0178 in Rust: Conflicting implementations of the same trait for a type
  • Utilizing a Reactor Pattern in Rust for Event-Driven Architectures
  • Parallelizing CPU-Intensive Work with Rust’s rayon Crate
  • Managing WebSocket Connections in Rust for Real-Time Apps
  • Downloading Files in Rust via HTTP for CLI Tools
  • Mocking Network Calls in Rust Tests with the surf or reqwest Crates
  • Rust - Designing advanced concurrency abstractions using generic channels or locks
  • Managing code expansion in debug builds with heavy usage of generics in Rust
  • Implementing parse-from-string logic for generic numeric types in Rust
  • Rust.- Refining trait bounds at implementation time for more specialized behavior