Russian Dolls and clean Rust code

Published November 28th, 2016

Recently I started porting my website from Github pages to this domain. I wanted to make my own static site using Iron and code it all myself as a challenge rather then use a Jekyll template. The fact you're reading this means that succeeded! However, that's an article for another time. Today I wanted to talk a little about making your Rust code more readable as well as resources for better patterns and cleaner code.

The Russian Doll Problem

As part of the site I use a toml configuration file and I read certain values from it. Thing is with an enum representing toml values with Rust values inside and some of those being BTreeMaps representing toml tables, well, it got pretty hairy in terms of unwrapping values I actually wanted. Since everything was using Option I had a lot of matching and unwrapping of values going on. My code was slowly devolving into this:

Russian Dolls

Here's what it looked like before in fact. Not one of my prouder coding moments:

// type Config = BTreeMap<String, toml::Value>

pub fn css(conf: &Config) -> Option<PreProc> {
    match conf.get("css") {
        Some(&Value::Table(ref tab))=> {
            match tab.get("pre_processor") {
                Some(&Value::Boolean(pp)) => {
                    if pp {
                        match tab.get("css_processor") {
                            Some(&Value::String(ref css_proc)) => {
                                if css_proc == "sass" {
                                    Some(PreProc::Sass)
                                } else if css_proc == "less" {
                                    Some(PreProc::Less)
                                } else {
                                    None
                                }
                            },
                            _ => None,
                        }
                    } else {
                        None
                    }
                },
                _ => None,
            }
        },
        _ => None,
    }
}

pub fn update_duration(conf: &Config) -> u64 {
    let sleep_default = 5;

    match conf.get("site"){
        Some(&Value::Table(ref tab)) => {
            match tab.get("sleep_update") {
                Some(&Value::Integer(val)) => {
                    val as u64
                },
                _ => sleep_default,
            }
        },
        _ => sleep_default,
    }
}

Gross right? Worst part is that was the cleaner version. Repeated patterns, just a bunch of unwrapping through matches, it's got a whole load of code smells. It was bothering me at work all day today. It was just sitting there giving me that feeling you get when you've committed a code sin but you're unsure how to atone for it. That all changed when I took a look at the Option docs on the train ride home. Upon finding the function I needed I had kicked the code smell out to the curb on my commute.

The Mr.Wolf of Option: and_then

If you're unfamiliar with Mr.Wolf from Pulp Fiction, he's a no nonsense man who can get you out of a bind. In this case I was deep in the code smell surrounded with nothing but Russian dolls and I needed all the help I could get.

Man did and_then get me to clean up my code. I went from the "clean" monstrosity you saw before to this:

pub fn css(conf: &Config) -> Option<PreProc> {
    conf.get("css")
        .and_then(Value::as_table)
        .and_then(|x| x.get("css_processor"))
        .and_then(Value::as_str)
        .and_then(|css_proc|
            if css_proc == "sass" {
                Some(PreProc::Sass)
            } else if css_proc == "less" {
                Some(PreProc::Less)
            } else {
                None
            })
}

pub fn update_duration(conf: &Config) -> u64 {
    let sleep_default = 5;

    conf.get("site")
        .and_then(Value::as_table)
        .and_then(|x| x.get("sleep_update"))
        .and_then(Value::as_integer)
        .unwrap_or(sleep_default) as u64
}

and_then works by taking in a function and acting on an Option. If it's Some it extracts the value and uses the function you passed to it, then rewraps it in a Some or it returns None if your passed in function does. If it's None it just passes back None. What's neat is that this allows us to chain together handling of Option values and change them with a function. In my case it was perfect because I wanted to transform these values if I was able to find them in the configuration but use None if I couldn't find the value.

Not only does this make the code not look like a Russian doll getting opened up, but it's more readable, and I can clearly see what each transformation might do.

When in doubt Std Lib out

The standard library contains a ton of great functions for dealing with things like Option or Result. I'm honestly surprised I hadn't looked them up first when I was having the problem. A new rule of thumb: If it feels awkward and wrong there's probably a function in the standard library to make it easier.

I've often found this to be the case when I've written Rust code and done some awkward god awful things. For example this is a pattern I did sometimes from when I first learned Rust:

let unwrapped: i32;

if value.is_some() {
  unwrapped = value.unwrap()
} else {
  unwrapped = 5;
}

The better way would have been this:

let unwrapped = value.unwrap_or(5);

If it feels weird or like an anti pattern there's probably a better way to do it. If you can't find a function in the standard library to resolve your issue I recommend looking at the patterns repo which can help with this.

Conclusion

Clean code is hard. It's easier to write things that just work without having a regard for what it looks like, especially for personal projects. However, this doesn't benefit anyone, including yourself. The good thing is there are resources like the patterns repo or clippy that can catch common mistakes to help you develop more rustic code.

If you're a newer coder or just new to Rust, I encourage you to use those resources (and the compiler!) to write better cleaner looking code. Spend the time making mistakes like this, to learn what not to do. As for the rest of the community, there's always more to learn. Over a year and a half of Rust and I'm still learning new things and I'm sure you are too! I would also encourage contributing to the pattern repo if you've been around for a bit. It could use some love and having examples of good patterns of rustic code for new users will be a major boon.

The good news is this site is currently running with the non Russian doll version! I hope this post encourages you to look at your code for areas you could clean up or make more readable. I know I will be!