Published on

Alternatives to Else in Rust

Authors
  • Mehdi Akiki avatar
    Name
    Mehdi Akiki
    Twitter

One of the first programming concepts most developers learn is the classic if/else statement. It allows code to make decisions based on certain conditions. However, as I gained more experience, I realized that while if statements are great for readability, else statements can sometimes cause unnecessary complexity.

Let's explore why over-reliance on else can be problematic and how Rust offers elegant alternatives.

The Problem with else

The core issue with else is the cognitive load it adds to our code. When reading an else block, we often have to scroll up to see the condition in the corresponding if block, holding that information in our minds while processing the code. This isn't much of a problem with small code blocks, but it becomes a significant issue in larger codebases.

Here's a common example in Rust (note that it applies to other languages too):

if want_some_toast() {
    butter_bread();
} else {
    make_a_sandwich();
}

This seems simple enough. But when the blocks grow larger and more complex, like in the following example:

if want_some_toast() {
    //... many lines of complex logic
} else {
    //... many lines of complex logic
}

The readability starts to degrade. You may have to scroll or mentally juggle information to understand the conditions under which each block is executed. This is where alternatives to else come in handy.

Alternatives to else in Rust

In Rust, we can reduce the need for else statements by using techniques like early returns, match expressions, and leveraging combinators like map and and_then. Let's dive into some of these approaches.

1. Early Returns

One simple way to eliminate else statements is by using early returns. Instead of having long conditional chains, we can return early when a condition is met, making the code more linear and easier to follow.

Without early returns:

fn get_jam_preference(name: &str) -> &'static str {
    let mut jam = "strawberry";
    if name == "joe" {
        jam = "marmalade";
    } else if name == "jane" {
        jam = "raspberry";
    }
    jam
}

With early returns:

fn get_jam_preference(name: &str) -> &'static str {
    if name == "joe" {
        return "marmalade";
    }
    if name == "jane" {
        return "raspberry";
    }
    "strawberry"
}

In the version with early returns, the code becomes more straightforward. Once we hit a return statement, we know the function is done, making the code flow easier to follow.

2. Using match Expressions

Rust's match expression is a powerful alternative to using if/else chains. It provides pattern matching capabilities and allows for clear, readable code when dealing with multiple conditions.

Using match:

fn get_jam_preference(name: &str) -> &'static str {
    match name {
        "joe" => "marmalade",
        "jane" => "raspberry",
        _ => "strawberry",
    }
}

The match expression is a clean and concise way to handle multiple conditions. It also enforces exhaustiveness checking, which ensures that all possible cases are covered, making your code safer and more robust.

3. Combinators like Option::map and Result::and_then

Rust's powerful Option and Result types come with combinators like map, and_then, and unwrap_or_else, which allow us to avoid the need for else in many scenarios.

Example with Option::map:

fn get_jam_preference_opt(name: Option<&str>) -> &'static str {
    name.map(|n| match n {
        "joe" => "marmalade",
        "jane" => "raspberry",
        _ => "strawberry",
    }).unwrap_or("strawberry")
}

Here, we use map to transform the Option<&str> and provide a default value using unwrap_or. This approach is concise and leverages Rust’s type system to avoid if/else altogether.

4. The Ternary-like Operator: if let

Rust doesn’t have a traditional ternary operator, but if let provides a similar pattern that can be used to simplify assignments based on conditions.

Using if let:

fn get_jam_preference(name: Option<&str>) -> &'static str {
    if let Some("joe") = name {
        "marmalade"
    } else if let Some("jane") = name {
        "raspberry"
    } else {
        "strawberry"
    }
}

This pattern is particularly useful when working with options, as it provides a cleaner, more readable way to handle conditions.

5. Pattern Matching with Guards

Rust’s pattern matching can be even more powerful when combined with guards, which are additional conditions in match arms.

fn get_jam_preference(name: Option<&str>, likes_sweet: bool) -> &'static str {
    match name {
        Some("joe") if likes_sweet => "marmalade",
        Some("joe") => "butter",
        Some("jane") => "raspberry",
        _ => "strawberry",
    }
}

Here, match checks both the name and an additional condition (likes_sweet). This lets us write expressive and complex logic without needing else.

Conclusion

Rust provides powerful tools like early returns, match expressions, and combinators that help reduce the need for else statements, making code simpler and easier to read. By leveraging these alternatives, we can write more maintainable and clear code, minimizing cognitive load and improving overall program flow. Experimenting with these patterns can lead to cleaner, more efficient Rust code.

I build and scale reliable production systems. Open to full-time and freelance work with U.S.-based teams that value ownership and execution.

Got something in mind?

Book a Discovery Call