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
- A talk given by Stanford’s very own John Ousterhout
- The CS190 Lectures (Software Design Studio)
Resources on and Tools for Good Rust Style
- A general rust style guide
- Use the rustfmt tool to make your code a e s t h e t i c .
- Clippy is another tool that can make your code a e s t h e t i c .
- Read here for guidelines on how to write proper documentation for your Rust code e.g. special comment syntax.
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),
}
}
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 enum
s
You may have seen enum
s before, but chances are that Rust enum
s 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
enum
s in the Rust book.
It also bears mentioning that you can make enum
s 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 enum
s 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 String
s 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.