Style Guide

Purpose

This page will have some resources for good systems programming style/design as well as good Rust style. If you find other helpful resources, please let us know over Slack and we’ll gladly add them here.

Resources on General Style/Software Design Philosophy

Resources on and Tools for Good Rust Style

Style points Specific To Rust

Thanks to Will Crichton for providing suggestions here!

Dealing with Option<T> and Result<T, E>

By this point, you’ve dealt with Option<T> and Result<T, E> in various error handling contexts and in implementing data structures (for instance our linked list example. Generally, any dynamic data structure involving pointers will require Option<T> or an enum since we need to somehow represent the idea of a NULL pointer). In this section, we’ll go over some fancy syntax for handling these types (and a tool like Clippy may chastise you for not using this syntax).

Traditionally, you’d deal with Option<T> and Result<T, E> by using a match statement like this:

use std::fmt;
pub fn print_if_something<T: fmt::Display>(thing_opt: &Option<T>) {
    match thing_opt {
        Some(thing) => println!("{}", thing),
        None => println!("oof nonething :("),
    }
}

pub fn print_if_okthing<T, E>(thing_res: &Result<T, E>)
where
    T: fmt::Display,
    E: fmt::Display,
{
    match thing_res {
        Ok(thing) => println!("{}", thing),
        Err(e) => println!("oof not ok :( because {}", e),
    }
}

Playground link here

Here’s some slicker syntax for dealing with Option using good ol if statements:

pub fn print_if_something<T: fmt::Display>(thing_opt: &Option<T>) {
    if let Some(thing) = thing_opt {
        println!("{}", thing)
    } else {
        println!("oof nonething :(")
    }
}

You can do something similar to this for Result:

pub fn print_if_okthing<T, E>(thing_res: &Result<T, E>)
where
    T: fmt::Display,
    E: fmt::Display,
{
    if let Ok(thing) = thing_res {
        println!("{}", thing)
    } else if let Err(e) = thing_res {
        println!("oof not ok :( because {}", e)
    }
}

However, note that here you’re not winning much by using if since you have to match on the error case too. If you didn’t have to capture what the Err contains, then it’s Ok to use the if let syntax.

More generally, you can use if let syntax on enums: you can read more about that here.

And in case you were wondering it… yep, it works in while loops too. Look no further than our example of the Drop trait from lecture 5:

impl Drop for LinkedList {
    fn drop(&mut self) {
        let mut current = self.head.take();
        while let Some(mut node) = current {
            current = node.next.take();
        }
    }
}

The condition of the while loop evaluates to true if we can successfully match current to Some(mut node). Once we hit the end of the linked list and we get a None, the while loop halts.

As we saw in lecture, we can use special syntax in functions when our function itself returns an Option or a Result. Here’s a snippet from lecture 4:

pub fn pop(&mut self) -> Option<u32> {
  let node = self.head.take()?;
  self.head = node.next;
  self.size -= 1;
  Some(node.value)
}

The first line of the function says “try to match self.head.take() to Some(node)”. If you can’t, immediately return None from the function. If you can, bind the value contained within the Some to the variable node.

Rust enums

You may have seen enums before, but chances are that Rust enums are more powerful than what you’d get in C/C++ (although they do have a similar concept called a union, which are unfortunately not typesafe). Traditionally, an enum would be used to represent a data type that has a small number of finite variants. For instance, we may use an enum to represent the direction a character is facing in some simple grid game. In Rust, it would look like this:

pub enum Direction {
    North, South, East, West
}

In a way, it’s like defining a new struct. Here’s how you might use it in a more complete implementation:

#[derive(Clone, Copy, Debug)]
pub enum Direction {
    North, South, East, West
}

#[derive(Clone, Copy, Debug)]
pub struct Location {
    row: usize,
    col: usize,
}

pub trait GameEntity {
    fn get_orientation(&self) -> Direction;
    fn get_location(&self) -> Location;
}


pub struct Player {
    name: String,
    orientation: Direction,
    location: Location,
}

impl GameEntity for Player {
    fn get_orientation(&self) -> Direction {
        self.orientation
    }
    
    fn get_location(&self) -> Location {
        self.location
    }
}

As you can see, you can derive traits (and implement traits) for your enum! Nice! Ok, but how do you actually read the value of your enum? You do that by using a match statement. You can read more about using match statements with enums in the Rust book.

It also bears mentioning that you can make enums generic as we saw in lecture with the MyOption example, and you can plug values into them:

pub enum MyOption<T> {
    Sumthin(T), Nuthin
}

impl<T: fmt::Display> fmt::Display for MyOption<T> {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            MyOption::Sumthin(num) => write!(f, "Sumthin({})", num),
            MyOption::Nuthin => write!(f, "Nuthin :("),
        }
        
    }
}

Deciding when and where to use enums is something you’ll develop a taste for. In general, when you want to define one type that can have many possible variants and these variants can even be struct-like (e.g. having their own fields), that’s when you should use an enum.

Use Iterators!

If a collection has an iterator, use it! Doing so in some cases allows the compiler to optimize your code better (e.g. by optimizing some checks away). If you’re implementing a collection of some sort, you should also implement an iterator over it (as you may have done in the weekly exercises). When implementing an iterator for some collection called Things<T>, you should implement the IntoIterator trait for Things<T>, &Things<T>, and &mut Things<T>. This will additionally require you to define (perhaps a couple) of structs that implements the Iterator trait (you also saw this in the week 3 exercises). Iterators are super rad because they give you a whole variety of additional methods for free! Wow! And a lot of these methods play nicely with Rust’s functional syntax.

Functional Syntax

Rust supports functional features like lambda functions/closures as well as the constructs that operate with them e.g. map, filter, reduce, etc. You can read more about Rust’s functional features here. These features are commonly used when operating with collections and iterators, e.g. if you want to square everything in a Vec<u32> or you want to iterate over all Strings in a Vec<String> that start with a particular character. You can even use these functional features on less collection-y things like Option<T>. A prime example of this is map_or in Option<T>, which you can read about here.

Minimize your Use of Unsafe!

When working on a sufficiently complex/low-level system, you’ll inevitably want to write code that Rust’s compiler just won’t like, e.g. managing raw memory. Even core Rust constructs such as smart pointers (e.g. Box<T>, Rc<T>, RefCell<T>, etc.) need to do unsafe things under the hood (in the case of smart pointers, inserting calls to free when appropriate). In order to break these safety guarantees, you’ll wrap your unsafe code in an unsafe {...} block. In general, you should avoid writing unsafe code if you can help it, but if you absolutely must, try to make these code segments as short and simple as possible. If you’d like to see some examples of how to do unsafe right, take a look at this book, which will walk you through implementations of Vec, Arc, and Mutex.

We’ll talk about unsafe in week 8.