Swap Logic: Making One Function Do Many Things in Rust

When you’re writing Rust functions, it’s tempting to bake specific behaviour directly into the implementation. But what happens when requirements change? You end up with duplicate functions or sprawling conditional logic. There’s a better way: separate the mechanism from the policy.

The Problem: Hardcoded Behaviour

Consider a simple event processor that extracts payloads from a list of events:

struct Event {
    kind: String,
    payload: i32,
}

fn process_events(events: Vec<Event>) -> Vec<i32> {
    events.iter().map(|e| e.payload).collect()
}

This works fine—until you need to double the payload values. Or extract the length of each event’s kind string. Suddenly, you’re either duplicating process_events with slight variations or adding parameters and conditionals that make the function increasingly brittle.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=c01c3a0e3b66718765b763a29ef62cfe

The Solution: Inject the Behaviour

Instead of hardcoding what to do with each event, pass that logic in as a parameter. Rust’s closures make this elegant:

fn process_events<F>(events: Vec<Event>, handler: F) -> Vec<i32>
where
    F: Fn(&Event) -> i32,
{
    events.iter().map(|e| handler(e)).collect()
}

// Now the caller decides:
process_events(events.clone(), |e| e.payload);        // extract payload
process_events(events.clone(), |e| e.payload * 2);    // double it
process_events(events, |e| e.kind.len() as i32);      // measure kind

The function now handles only the mechanics—iteration and collection. The actual transformation logic lives in the caller-provided closure. This is the essence of mechanism/policy separation: the library provides the plumbing, the user provides the logic.

Why This Matters

This pattern isn’t just about avoiding duplication. It’s fundamental to library design in Rust:

Flexibility: One function adapts to countless use cases without modification. Need to filter events first? Add logging? Access external state? Just change the closure.

Testability: You can test the mechanism independently from specific behaviors. Mock handlers become trivial to write.

Composability: Higher-order functions like this compose beautifully. Chain them together, wrap them in error handling, or integrate them into larger systems without touching the core logic.

You’ll see this pattern throughout the Rust ecosystem. Web frameworks like Actix use it for request handlers. Iterator adapters like map and filter are built on it. Even async runtimes rely on user-provided futures to determine what work gets done.

From Rigid to Reusable

The shift from hardcoded behaviour to pluggable handlers is more than a refactoring technique—it’s a mindset. When you design functions that accept closures, you’re building APIs that grow with your users’ needs. Your code becomes a toolkit rather than a script, capable of solving problems you never anticipated.

Next time you write a function, ask yourself: am I hardcoding policy that could be injected? That small change often makes the difference between throwaway code and a reusable library component. In Rust, with zero-cost abstractions and powerful type inference, there’s no reason not to design for flexibility from the start.

Simplified version?

handlers

This is Rust code demonstrating higher-order functions and closures.

The Event Struct

struct Event {
    kind: String,
    payload: i32
}

A simple data structure holding a string kind and an integer payload.

The process_events Function

fn process_events<F>(events: Vec<Event>, handler: F) -> Vec<i32>
where F: Fn(&Event) -> i32

This is a generic function that:

  • Takes a vector of Event objects
  • Takes a function handler (type F) that accepts an &Event reference and returns an i32
  • The where clause constrains F to be any function matching that signature
  • Returns a vector of i32 values

The function body processes events by iterating through them, applying the handler to each event, and collecting the results into a new vector.

The main Function

Creates two events and then calls process_events with a closure:

|e| e.payload

This closure takes each event e and extracts its payload field. The circled annotations show:

  • The generic type F gets filled in with this closure type
  • The closure is passed as the handler parameter
  • It extracts the payload from each event

So res ends up being vec![12, 24] – just the payload values from both events.

This pattern is common in Rust for creating flexible, reusable functions that can work with different processing logic supplied by the caller.