Phil Booth

Existing by coincidence, programming deliberately

The elegance of Rust

Here's three little Rust tricks that I learned in the last week or so. Each struck me as being an elegant approach to working cleanly within the confines of a strongly-typed language.

  1. Say you have a bunch of custom error types in different places, each tailored to the specific requirements of some module or function. When errors from lower levels bubble up through higher ones, they need to be transformed to the correct type. What's a nice way to do that?

    The solution comes in two parts. Firstly, implement the From trait for the higher-level error, specifying the lower-level error as the type argument:

    impl From<DbError> for ApiError {
        fn from(value: DbError) -> ApiError {
            ApiError {
                // ...
            }
        }
    }
    

    Secondly, call Result::map_err when you want to transform the lower-level error into the higher-level one:

    pub fn do_something(&self) -> Result<Foo, ApiError> {
        // db.query returns Result<Foo, DbError>
        self.db.query(self.query)
            .map_err(From::from)
    }
    
  2. Sticking with the Result theme, what should you do if you want to fold/reduce over an iterator using a function that can fail?

    Coming from a JS background, in the past I might have thrown from reduce and silently cursed myself over using throw for flow control. But in Rust, Iterator::try_fold comes to the rescue:

    let remaining_pizza = pizza_slices
        .iter()
        .try_fold(0, |consumed, slice| {
            if consumed + slice >= 360 {
                Err(())
            } else {
                Ok(consumed + slice)
            }
        })
        .map(|consumed| 360 - consumed);
    
  3. Maybe there are parts of your code that can only be tested if you use mocks to force execution along specific paths. Or maybe you want to use mocks for other reasons, like making your tests faster or just ensuring that failures are properly isolated. At first this can seem tricky in a strongly-typed language, but the end result is actually better and more robust than a dynamically-typed language can achieve.

    The key is to promote the type to a trait:

    pub trait Emailer {
        fn send_email(&self, message: EmailMessage) -> Result<u64, EmailError>;
        fn get_delivery_status(&self, message_id: u64) -> Result<EmailStatus, EmailError>;
    }
    
    pub struct EmailClient {
        // ...
    }
    
    impl EmailClient {
        pub fn new() -> EmailClient {
            EmailClient {
                // ...
            }
       }
    }
    
    impl Emailer for EmailClient {
        fn send_email(&self, message: EmailMessage) -> Result<u64, EmailError> {
            // ...
        }
    
        fn get_delivery_status(&self, message_id: u64) -> Result<EmailStatus, EmailError> {
            // ...
        }
    }
    

    Then, if you change the code that uses EmailClient to expect Box<Emailer> instead, you can create all kinds of weird and wonderful mock email clients in your test modules:

    pub struct EmailMockFailsOnSend;
    
    impl Emailer for EmailMockFailsOnSend {
        fn send_email(&self, message: EmailMessage) -> Result<u64, EmailError> {
            Err(EmailError::new("wibble"))
        }
    
        fn get_delivery_status(&self, message_id: u64) -> Result<EmailStatus, EmailError> {
            // ...
        }
    }
    
    #[test]
    fn test_some_behaviour_when_email_send_fails() {
        let result = super::do_something(Box::new(EmailMockFailsOnSend));
        // ...
    }
    

    The really nice thing about this approach is that now your mock objects are all strongly-typed too. If someone changes the Emailer trait, they can't forget to update the mock implementations because the build will fail. Dynamically-typed languages might make mocking easier, but they can't offer that guarantee.