Rust: Why Use impl Instead of Standalone Functions?

When developing in Rust, one question that often arises is whether to use impl blocks to define methods on structs or to use standalone functions. Well at least it does to me! 😊

Both approaches have their merits, but using impl provides several distinct advantages, particularly when working with structs. Let’s explore this with a concrete example and some key reasons why impl is often the preferred choice.

By the way, this thought occurred to me whilst working with Rig, which is an interesting AI framework for Rust, you might like it!

So here’s the Rust Playground code example to demonstrate

impl vs fn

Example: Adding Numbers with impl

Here’s an example that showcases a struct called AddArgs and how we can use an impl block to add functionality to it:

// Define the AddArgs struct
#[derive(Debug, PartialEq)]
struct AddArgs {
    a: i32,
    b: i32,
}

// Type alias for AddArgs
type Args = AddArgs;

impl Args {
    // Method to perform addition
    fn add(&self) -> i32 {
        self.a + self.b
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_vec_of_args() {
        // Create a vector of Args
        let args_vec = vec![
            Args { a: 1, b: 2 },
            Args { a: 3, b: 4 },
            Args { a: 5, b: 6 },
        ];

        // Collect results of addition
        let results: Vec<i32> = args_vec.iter().map(|args| args.add()).collect();

        // Verify the results
        assert_eq!(results, vec![3, 7, 11]);
    }
}

Why Use impl Instead of Standalone Functions?

Here are the main advantages of using impl to define methods directly on a struct:

1. Tightly Coupled Behavior with the Struct

When you use impl, the methods become directly associated with the struct. This makes it clear that the behavior (e.g., addition in this case) is specifically for instances of AddArgs.

For example:

impl Args {
    fn add(&self) -> i32 {
        self.a + self.b
    }
}

let args = Args { a: 2, b: 3 };
println!("{}", args.add()); // Clear and concise

By contrast, a standalone function would require explicitly passing the struct as an argument:

fn add(args: &Args) -> i32 {
    args.a + args.b
}

let args = Args { a: 2, b: 3 };
println!("{}", add(&args)); // Less direct

2. Improved Readability and Organization

Methods defined within impl blocks keep the logic tied to the struct itself. This avoids scattering related functionality across the codebase. If you’re working on Args, all its behavior is grouped in the impl block, improving organization.


3. Ergonomics

Method calls using impl let you use method chaining or easily combine operations without extra boilerplate:

impl Args {
    fn add(&self) -> i32 {
        self.a + self.b
    }
}

let args = Args { a: 10, b: 15 };
let result = args.add() + 5; // Natural syntax

By contrast, standalone functions require explicit passing of the struct, which can become verbose.


4. Encapsulation and Extensibility

Using impl allows you to encapsulate behavior and control visibility (e.g., pub, private). You can also add utility methods for Args while keeping its fields private to enforce invariants:

#[derive(Debug)]
struct Args {
    a: i32,
    b: i32,
}

impl Args {
    pub fn new(a: i32, b: i32) -> Self {
        Self { a, b }
    }

    pub fn add(&self) -> i32 {
        self.a + self.b
    }
}

let args = Args::new(10, 20); // Controlled creation
println!("{}", args.add());

5. Future-Proofing

If your struct’s behavior becomes more complex (e.g., supporting multiplication, subtraction, etc.), using impl makes it easy to add methods without needing to create standalone functions for each:

impl Args {
    fn add(&self) -> i32 {
        self.a + self.b
    }

    fn multiply(&self) -> i32 {
        self.a * self.b
    }
}

When to Use Standalone Functions Instead

Standalone functions are best when:

  1. The logic isn’t inherently tied to a specific struct or type.
  2. The function operates on multiple types or is more generic.

Example:

fn add_generic<T: Add<Output = T>>(a: T, b: T) -> T {
    a + b
}
  1. You want maximum flexibility or a functional programming style.

Conclusion

Using impl is preferable when the functionality logically belongs to the struct itself, improving readability, ergonomics, and maintainability. Standalone functions, however, are great for generic or unrelated operations. By choosing the right approach, you can make your Rust code more intuitive and efficient.