Rust Concurrency Explained: Cleaning Code with Traits and Simplifying Services
Tags: rust, programming, concurrency, cleancode
Published: 2023-11-21 10:31:17

# Rust Concurrency: Cleaning Code with Traits and Simplifying Services

## Introduction

As I continue my journey with Rust, I've found that revisiting and reviewing my code is not just a best practice but a learning tool in itself. After getting my code to work, I like to take a step back and ponder if there are ways to enhance its readability and efficiency. This reflective practice often leads to significant improvements, making my code cleaner and more understandable.

Recently, I revisited the initial commit of my project, `rust_service_example` ([link to first commit on GitHub](https://github.com/ietxaniz/rust_service_example/tree/Initial)). While reviewing, I noticed some areas that didn't quite sit right with me. For instance, the process of read-locking and write-locking was taking up more lines than necessary, creating repetitive patterns that I think will cumbersome during code reviews. This realization led me to explore how traits could streamline these aspects, which I'll delve into later in this article.

Another area I've been questioning is my approach to error handling. Initially, I transitioned from returning a String describing the error to returning `Box<dyn Error>`, which seemed like a step in the right direction. However, I'm now experimenting with the `anyhow` crate, seeking a more refined method, although I'm still evaluating if it's the best fit.

Lastly, I made a small change in the main code. By changing the field types in my `Service` struct from `RwLock<T>` to `Arc<RwLock<T>>`, I was able to implement the `Clone` trait. This change has simplified the code in `main.rs`, making it more approachable, especially from a beginner's perspective.

For those interested in seeing the code and its evolution, feel free to explore the repository on [GitHub](https://github.com/ietxaniz/rust_service_example).

In this article, I'll share these refinements and discuss how they contribute to better, cleaner Rust code. Join me as I navigate through these improvements, and let's learn together how small changes can make a big difference.

## Refining Error Handling

In my journey of transitioning from Go to Rust, I've come to realize the nuances in how both languages approach error handling. In my previous articles, I drew parallels between Rust and Go in terms of verbosity and explicitness in managing errors. However, with deeper exploration, I've discovered some key differences and improvements in my Rust coding practice, particularly regarding error handling.

### Revisiting Rust vs. Go in Error Handling

In Go, the convention of functions returning both a value and an error is a pattern I've always appreciated for its clarity. This approach makes error handling an integral and explicit part of the function's contract. Initially, I viewed Rust's error handling through a similar lens, especially when using `match` statements to handle `Result` types.

Rust, however, offers a more streamlined approach with the `?` operator. This operator allows for elegant error propagation, reducing the verbosity that comes with manual error checking. Unlike the explicit handling in Go, Rust's `?` operator simplifies the code, making it less verbose while still maintaining clarity and predictability.

### The Shift to `anyhow` in Rust Error Handling

My initial foray into Rust error handling involved returning strings as errors. While this method was straightforward, it wasn't in line with Rust's standard practices. I soon learned that a more idiomatic approach in Rust is to return `Box<dyn Error>`. This standard has its advantages, primarily its flexibility and compatibility with Rust's error propagation mechanisms.

#### Why `Box<dyn Error>`?

In Rust, `Box<dyn Error>` is commonly used for error handling, similar to how functions in Go often return an error type. Just like in Go, where a function can return a result and an error, `Box<dyn Error>` in Rust provides a dynamic way to handle various error types uniformly. This approach is particularly beneficial in Rust because of the language's `?` operator, which enables seamless error propagation.

Using `Box<dyn Error>` in Rust is akin to Go's error return type in that it allows for a flexible response to different error scenarios. However, Rust's `?` operator simplifies error handling further. By adding `?` at the end of an operation, Rust can elegantly handle errors without the verbosity typical in Go, where each error requires explicit checking and handling.

#### The Challenge of Mixed Error Types

However, a challenge arises when there's a mix of error types being returned - sometimes a string, other times a boxed error. This inconsistency necessitates additional code for handling and converting errors, leading to a less streamlined and more verbose approach.

#### Embracing `anyhow` for Streamlined Error Handling

Recognizing this, I turned to the `anyhow` library for a more unified and efficient error handling strategy. `anyhow` simplifies the process by allowing the use of a single error type across the codebase. This aligns well with Rust's philosophy of concise and clear error handling while providing the flexibility to handle a wide range of error scenarios.

Using `anyhow`, I can write code that's consistent in its error handling approach, enhancing readability and maintainability. Moreover, `anyhow` integrates seamlessly with Rust's `?` operator, further reducing verbosity and complexity.

#### The Importance of Consistency and Adaptability

In conclusion, the key takeaway from my experience with error handling is the importance of consistency. Whether it's using `Box<dyn Error>`, `anyhow`, or another method, sticking to a consistent approach is crucial. It's also essential to remain open to refining your error handling strategy as you gain more insight into Rust's best practices and the tools available. This flexibility allows for continuous improvement in writing more effective and idiomatic Rust code.

### Practical Benefits of Using `anyhow`

Implementing `anyhow` in my Rust projects has brought several advantages:

- **Simplified Error Propagation**: With `anyhow`, I can easily return errors from functions without worrying about their specific types. The `?` operator works seamlessly with `anyhow::Error`, further reducing boilerplate code.

- **Enhanced Readability**: The use of a single error type declutters the code, making it more readable and maintainable. It's easier to understand and handle errors when they're consistently represented.

### Continuous Learning and Refinement

This exploration into Rust's error handling is a testament to the ongoing learning process in programming. What seemed like an established understanding can evolve as you delve deeper into a language's features and best practices. By embracing libraries like `anyhow` and utilizing Rust-specific features like the `?` operator, I'm refining my approach to writing more idiomatic and efficient Rust code.

## Refactoring the `Service` Struct for Simplified Usage

In the process of evolving the Rust codebase, a significant refactor was made to the `Service` struct. This change not only streamlined the code but also enhanced its readability and maintainability. Let's explore the transformation of the `Service` struct and how it positively impacted the usage in `main.rs`.

### Original `Service` Struct

Initially, the `Service` struct was defined as follows:

```rust
pub struct Service {
    count: std::sync::RwLock<u64>,
    count_thread1: std::sync::RwLock<u64>,
    count_thread2: std::sync::RwLock<u64>,
    write_lock: std::sync::RwLock<u8>,
}
```

This structure effectively managed the state using Rust's `RwLock` for thread-safe mutable access. However, when it came to sharing `Service` instances across threads, the usage in `main.rs` required explicit handling of `Arc` (Atomic Reference Counting pointers) to manage the shared ownership.

## The Refactored `Service` Struct

To simplify this pattern, the `Service` struct was refactored as follows:

```rust
#[derive(Clone)]
pub struct Service {
    count: Arc<RwLock<u64>>,
    count_thread1: Arc<RwLock<u64>>,
    count_thread2: Arc<RwLock<u64>>,
    write_lock: Arc<RwLock<u8>>,
}
```

By incorporating `Arc<RwLock<T>>` directly into the struct and deriving the `Clone` trait, the `Service` struct became more ergonomic to use. This change abstracts away the explicit handling of `Arc`, making the struct more straightforward to clone and share across threads.

### Simplified Usage in `main.rs`

#### Before the Refactor

Originally, sharing an instance of `Service` across threads required explicitly creating an `Arc` and then cloning it:

```rust
let service = Arc::new(Service::new());

let service1 = Arc::clone(&service);
let service2 = Arc::clone(&service);
```

#### After the Refactor

With the refactored `Service` struct, the code in `main.rs` becomes cleaner:

```rust
let service = Service::new();

let service1 = service.clone();
let service2 = service.clone();
```

This refactoring makes the code more intuitive and straightforward. The cloning of the service instance is now a simple method call, enhancing the overall readability.

## Using Traits for Code Simplification and Cleaning

One of the most powerful features of Rust is its trait system, which allows for code abstraction and reuse in a way that's both efficient and elegant. In this part of the article, I'll demonstrate how we can leverage traits to simplify and clean up our code. Our goal is to transform a common pattern in our codebase into a more concise and readable form, without altering the application's functionality.

### The Objective: Streamlining Lock Operations

Consider this snippet from our current codebase:

```rust
let count = *self
            .count
            .read()
            .map_err(|e| format!("Failed to read-lock count: {}", e))?;
```

Here, we're acquiring a read lock on a resource and handling potential errors. While this code is functional, it's also somewhat verbose and repetitive, especially if similar patterns are used throughout the application.

Now, let's look at how we can streamline this with the help of a custom trait:

```rust
let count = *self.count.lock_read("count")?;
```

With this new approach, the code becomes much more succinct and clear. The error handling is still there, but it's abstracted away by our trait, making the main logic easier to read and maintain.

### Simplifying Lock Operations with the `LockExt` Trait in Rust

In Rust, managing locks, especially with `RwLock`, can often involve verbose and repetitive error handling. To address this, let's explore the `LockExt` trait, a solution that streamlines these operations. This trait is a great example of how Rust's powerful trait system can be used to enhance code readability and efficiency.

Here's the code for the `LockExt` trait:

```rust
use anyhow::{anyhow, Error};
use std::sync::{Arc, RwLock};
use std::sync::{RwLockReadGuard, RwLockWriteGuard};

pub trait LockExt<T> {
    fn lock_write(&self, name: &str) -> Result<RwLockWriteGuard<T>, Error>;
    fn lock_read(&self, name: &str) -> Result<RwLockReadGuard<T>, Error>;
}

impl<T> LockExt<T> for Arc<RwLock<T>> {
    fn lock_write(&self, name: &str) -> Result<RwLockWriteGuard<T>, Error> {
        self.write()
            .map_err(|e| anyhow!("Failed to write-lock {}: {}", name, e))
    }

    fn lock_read(&self, name: &str) -> Result<RwLockReadGuard<T>, Error> {
        self.read()
            .map_err(|e| anyhow!("Failed to read-lock {}: {}", name, e))
    }
}
```

This trait abstracts the process of acquiring read and write locks, simplifying error handling. Instead of writing lengthy error handling every time a lock is acquired, the `LockExt` trait encapsulates this in two concise methods: `lock_write` and `lock_read`. And allows the simplification we where looking for at the beginning of this section.

### Emphasizing the Choice of `anyhow` in the `LockExt` Trait

Having previously discussed the nuances of error handling in Rust and the advantages of utilizing libraries like `anyhow`, the implementation of the `LockExt` trait further exemplifies why `anyhow` was the preferred choice. This trait demonstrates the practical application of `anyhow` in a real-world scenario, highlighting its benefits in streamlining error handling.

#### Simplified Error Handling with `anyhow`

Consider the `lock_write` function using `anyhow`:

```rust
use anyhow::{Result, anyhow};
use std::sync::{RwLockWriteGuard, RwLock};

fn lock_write<T>(&self, lock: &RwLock<T>, name: &str) -> Result<RwLockWriteGuard<T>> {
    lock.write()
        .map_err(|e| anyhow!("Failed to write-lock {}: {}", name, e))
}
```

This implementation leverages `anyhow` for its concise and expressive error reporting. The `anyhow!` macro enables quick conversion of errors into an `anyhow::Error`, complete with a descriptive message. This approach significantly reduces boilerplate and enhances readability.

Contrast this with the more traditional approach without `anyhow`:

```rust
use std::sync::{RwLockWriteGuard, RwLock};
use std::error::Error;
use std::fmt;

struct MyError {
    details: String,
}

// Implementations for MyError...

fn lock_write<T>(&self, lock: &RwLock<T>, name: &str) -> Result<RwLockWriteGuard<T>, Box<dyn Error>> {
    lock.write()
        .map_err(|e| Box::new(MyError::new(&format!("Failed to write-lock {}: {}", name, e))) as Box<dyn Error>)
}
```

Here, the necessity of a custom error type (`MyError`) and the additional code for error management increases the complexity, making the function more verbose.

## Conclusion

### Consistent Error Handling with `anyhow`

The transition to using `anyhow` for error handling in Rust represents more than just a technical refinement; it signifies an embrace of idiomatic Rust practices that prioritize clarity, brevity, and robustness. This shift not only made the error handling in my codebase more consistent but also underscored the importance of adaptability and continuous learning in software development. As Rust continues to evolve, so too should our approaches to coding within its ecosystem. The adoption of `anyhow` is a testament to this philosophy, showcasing a commitment to write code that is not only functional but also clean and maintainable.

### The Impact of Refactoring `Service` Struct

Embedding `Arc` directly into the `Service` struct and leveraging the `Clone` trait proved to be a subtle yet impactful change. This refactoring underscores the philosophy of smart struct design in Rust—where the focus is not just on the functionality but also on how the structure of the code can lead to more intuitive and elegant usage patterns. By making these adjustments, the `Service` struct became more aligned with Rust's design principles, offering a more streamlined and idiomatic way of handling shared state in concurrent environments.

### Leveraging Traits for Code Simplification

The implementation of the `LockExt` trait is a prime example of Rust's powerful trait system at work. It highlights how traits can be used not just for defining shared behavior, but also for simplifying and cleaning up code. This approach is particularly useful in Rust, where managing complexity is key to writing effective programs. By abstracting repetitive patterns into a trait, the codebase becomes more organized, allowing for easier maintenance and future enhancements. The `LockExt` trait, therefore, is not just a utility but a representation of the Rust philosophy of making code more modular and expressive.