Let’s start using Rust in a way that shows differences from other imperative languages. Enumerations, also called algebraic or composable types, to help fully express the domain. Structs with public methods for encapsulation and abstraction. Traits to express common interfaces and support Rust language features.

Rust programmers create values (or feel free to call them objects) are created at once and in a valid form. You can use an associated method, often called new() or simliar, as a pub constructor of your struct values. The struct itself must be marked pub as well but the member fields can stay private.

Constructors like new() should return Self when object creation is not supposed to fail or when panic!() is an acceptable outcome. In a library, when new() can fail, it should instead return Result<Self, _> where the second type argument is an error type.

Simple enumerations

Rather than integers and magic constants, let’s use convenient enumerations whenever possible. Use the Debug trait to make your life easier.

enum Piece {
    King,
    Queen,
    Bishop,
    Knight,
    Rook,
    Pawn,
}

fn main() {
    let piece = Piece::King;
    println!("{:?}", piece);
}

Tuples

The simplest way to create compound types and values is to design a small heterogenous sequence called a tuple. Use tuples for quick coupling of values whenever it doesn’t make sense to create an abstract struct type.

#[derive(Debug)]
enum Color {
    White,
    Black,
}

fn main() {
    let king = (Piece::King, Color::White);
    println!("{:?}", king);
}

Enumeration with data

Use enumerations with attached data to store more complex data structures. Note that all enumeration variants are public. When you need encapsulation, you need to put your enum in a struct.

#[derive(Debug)]
enum Item {
    White(Piece),
    Black(Piece),
}

fn main() {
    let king = Item::White(Piece::King);
    println!("{:?}", king);
}

Fixed size arrays

Use arrays when you know the size at the compile time. Make your types Copy in order to create short fixed size arrays using the repeat syntax.

#[derive(Debug, Clone, Copy)]
enum Piece {
    King,
    Queen,
    Bishop,
    Knight,
    Rook,
    Pawn,
}

fn main() {
    let row = [Piece::King; 8];
    println!("{:?}", row);
}

You can use the same technique for multi-dimensional arrays.

fn main() {
    let row = [Piece::King; 8];
    let board = [row; 8];
    println!("{:?}", board);
}

Now you have a proper chess board full of kings. You can use the previous examples to add support for piece colors as well as empty fields.

Option enumeration

Rust avoids the “billion dollar mistake” at all costs. References and smart pointers in Rust are not nullable. Whenever you need a nullable function argument or nullable struct field you can use Option<_> to wrap your non-nullable type in an enum that contains either Some(value) or None.

fn main() {
    let mut board = [[None; 8]; 8];
    board[0][0] = Some(Piece::Rook);
    println!("{:?}", board);
}

You can see how Rust deduces that Some(Piece::Rook) will be of type Option<Piece> and therefore the None value in the array initialization is a valid value of the above type.

Result enumeration

Another standard enumeration template expresses a result of a fallible operation. It can hold a resulting value as Ok(value) or it can hold an error Err(error). You can create your own Error enumeration to express possible error outcomes of your operations.

#[derive(Debug)]
enum Error {
    InvalidRow,
    InvalidColumn,
}

fn replace_field(board: &mut Board, row: usize, col: usize, piece: Piece) -> Result<Piece, Error> {
    if !(0..7).contains(&row) {
        Err(Error::InvalidRow)
    } else if !(0..7).contains(&col) {
        Err(Error::InvalidColumn)
    } else {
        let orig = board[row][col];
        board[row][col] = piece;
        Ok(orig)
    }
}

fn main() {
    let mut board = [[Piece::King; 8]; 8];
    let orig = replace_field(&mut board, 0, 0, Piece::Rook).unwrap();
    println!("{:?}", board);
    println!("{:?}", orig);
}

Pattern matching

One of the biggest strengths of Rust is pattern matching. You need to match all possible variants or you need to provide an action for the default case.

Pattern matching branches return a value that must be of the same type in all branches. You can escape that rule with either return that prevents returning from the match statememt or with panic!() that simply ends the control flow. Otherwise the return value type must be satisfied but again you can use enumerations including Option and Result.

fn describe(item: &Item) {
    let (color, piece) = match item {
        Item::White(piece) => ("white", piece),
        Item::Black(piece) => ("black", piece),
    };
    let piece = match piece {
        Piece::Queen => "queen",
        Piece::King => "king",
        _ => "something",
    };
    println!("This is a {} {}.", color, piece);
}

fn main() {
    let item = Item::Black(Piece::Queen);
    describe(&item);
}

Simple error handling

In a simple script-like program you don’t need to always analyze all the errors that can happen. You can just assume errors are inrecoverable and simply turn them all into panic!().

fn main() {
    // Just terminate.
    panic!();
}

Failed dynamic memory allocation panics. Any use of .unwrap() or .expect() turns a bad Result or empty Option into panicking.

Proper error handling can be done using pattern matching.

fn main() {
    let mut board = [[Piece::King; 8]; 8];
    match replace_field(&mut board, 0, 0, Piece::Rook) {
        Ok(orig) => {
            println!("{:?}", board);
            println!("{:?}", orig);
        }
        Err(error) => {
            println!("There was an error: {:?}", error);
        }
    }
}

If we’re not interested in the error details, there is a shortcut.

fn main() {
    let mut board = [[Piece::King; 8]; 8];
    if let Ok(orig) = replace_field(&mut board, 0, 0, Piece::Rook) {
            println!("{:?}", board);
            println!("{:?}", orig);
    } else {
        println!("There was an error.");
    }
}

Chained control flow

Please look at the Option and Result documentation and see what it can do for you. The methods you see can be used to avoid excessive use of match in cases where simple tranformation is easier to read and understand. Learn to easily chain many operations in a way that one of them can fail and the rest get simply skipped.

Together with the ? operator they can be used to perform a conditional return to the calling function if applied to an Err(error) variant of a Result. Use .ok_or()? and .map_err()? to convert Option and Result to the right type of result and return conditionally. When Ok(value) variant is present, the ? operator returns value.

Use tools like .transpose()? to get rid of the Result wrapper in Option<Result<_, _>>. Use simple closures (like lambdas in C++ but simpler) as function arguments. Use iterators to manipulate with linear data structures.

fn convert(value: &str) -> Result<(usize, usize), Error> {
    let [letter, number]: [char; 2] = value.chars()
        .collect::<Vec<_>>()
        .try_into()
        .map_err(|_| Error::InvalidPosition)?;
    let row = "12345678".chars().position(|n| n == number)
        .ok_or(Error::InvalidPosition)?;
    let column = "abcdefgh".chars().position(|c| c == letter)
        .ok_or(Error::InvalidPosition)?;
    let position = (row, column);
    Ok(position)
}

The above code takes the UTF-8 string like “e4”, turns into a char iterator, collects into a Vec<char>, uses it as a slice reference &[char], converts it into Result<[char; 2], Error>, unwraps the [char; 2] value and stores it into letter and number. Then converts these to usize also using iterators. Then returns a (usize, usize) tuple wrapped in a Result so that an Err(error) can be returned at any of the ? points.

Please do not forget that closures are functions and therefore a return inside a closure block returns only from the closure. When it’s too tricky to keep up with a complex chain of options, results and closures, just store the intermediate values. Feel free to Use mut vectors and for loops when the “functional” way turns out to be too difficult.

Structures

To provide encapsulation and proper abstraction you need to define structured types with struct.

#[derive(Debug, Clone, Copy)]
enum Piece {
    King,
    Queen,
    Bishop,
    Knight,
    Rook,
    Pawn,
}

#[derive(Debug, Clone, Copy)]
enum Color {
    White,
    Black,
}

#[derive(Debug, Clone, Copy)]
struct Item {
    color: Color,
    piece: Piece,
}

#[derive(Debug)]
struct Board {
    fields: [[Option<Item>; 8]; 8],
    current: Color,
}

fn main() {
    let board = Board {
        fields: [[None; 8]; 8],
        current: Color::White,
    };
    println!("{:?}", board);
}

This looks awful. You should be able to create the empty board using a simple function call. Let’s move board implementation details into an associated functions.

impl Board {
    fn new_empty() -> Board {
        Board {
            fields: [[None; 8]; 8],
            current: Color::White,
        }
    }
}

fn main() {
    let board = Board::new_empty();
    println!("{:?}", board);
}

Much better. Let’s make a method that can actually modify the structure. It is an associated function, just with a self mutable reference argument.

impl Board {
    fn new_empty() -> Board {
        Board {
            fields: [[None; 8]; 8],
            current: Color::White,
        }
    }
    fn replace_field(&mut self, row: usize, col: usize, value: Option<Item>) -> Result<Option<Item>, Error> {
        if !(0..7).contains(&row) {
            Err(Error::InvalidRow)
        } else if !(0..7).contains(&col) {
            Err(Error::InvalidColumn)
        } else {
            let orig = self.fields[row][col];
            self.fields[row][col] = value;
            Ok(orig)
        }
    }
}

fn main() {
    let mut board = Board::new_empty();
    match board.replace_field(0, 0, Some(Item { color: Color::Black, piece: Piece::Queen })) {
        Ok(Some(item)) => { println!("We replaced a {:?}.", item); }
        Ok(None) => { println!("We filled an empty field."); }
        Err(error) => { println!("There was an error of type {:?}.", error); }
    }
    println!("{:?}", board);
}

Now you have all the tools to build data structures with associated operations.

Text manipulation

Rust’s UTF-8 strings can be borrowed as &str. This applies to both static read-only string literals and heap-allocated string objects.

type List = Vec<String>;

fn add(list: &mut List, activity: &str) {
    list.push(activity.to_string());
}

fn main() {
    let mut list: List = Vec::new();

    add(&mut list, "Clean the room");
}

String objects implements Deref<Target=str> and therefore &String can be used as &str.

fn main() {
    let mut list: List = Vec::new();

    for item in ["Bread", "Butter", "Tea"] {
        add(&mut list, &format!("Buy {}", item));
    }
}

Output and formatting

Simple output:

fn main() {
    print!("Hello World!");
}

Printing string literals:

fn func(name: &str, place: &str) {
    println!("This is {} from {}.", name, place);
}

fn main() {
    let name = "Joe";
    let place = "Prague";

    func(name, place);
}

Note: This only works because name and place are already references.

More information:

https://doc.rust-lang.org/std/fmt

Advanced output

You can create allocated strings using string formatting instead of just printing.

use std::io::Write;

fn main() {
    let value = std::f32::consts::PI;
    let text = format!("Pi is {:.2}.\n", value);
    std::io::stdout().write_all(text.as_bytes()).unwrap();
}

Or using a neat shortcut:

use std::io::Write;

fn main() {
    let value = std::f32::consts::PI;
    write!(std::io::stdout(), "Pi is {:.2}.\n", value);
}

All you need is that your values implement the Display trait.

User input

Implement Python-like input function:

use std::io::BufRead;
use std::io::{self, Write};

fn input(prompt: &str) -> String {
    print!("{}", prompt);
    io::stdout().flush().unwrap();
    io::stdin().lock().lines().next().unwrap().unwrap()
}

fn main() {
    let name = input("What's your name: ");
    println!("Hello {}!", name);
}

Use input() for interactive command-line programs:

fn main() {
    let a: i32 = input("a = ").parse().unwrap();
    let b: i32 = input("a = ").parse().unwrap();
    let c = a / b;

    println!("{:?} / {} = {}", a, b, c);
}

Type debugging

You can debug the types either with specialized code editors using rust-analyze or directly with the compiler. If you don’t know the type of some value, just move it to a new variable with wrong explicit type specified.

fn main() {
    let value = "Joe";
    let _: () = value;
    // See the compiler error.
}

If you don’t want to break your program, you can turn your static type information into text that can be printed at runtime.

fn type_of<T>(_: &T) -> String { std::any::type_name::<T>().to_string() }

fn main() {
    let name = "Joe";

    println!("{}", type_of(&name));
}

In some cases rust uses internal abstract types for basic values that are only then turned into actual types.

fn main() {
    let value = 42;
    let _: () = value;
}

The compiler code will tell you that the value is an integer rather than actual i32 for example. This helps you ignore the concrete numeric types before you actually need to use them. Functions only use actual types and thus passing your integer to a function will collapse it into the numeric type required by the function.

Value debugging

You already saw a println!(…) macro use. The same macro can be used for debugging values that support it. All you need is to use {:?} instead of just {} to trigger a debugging print.

fn main() {
    let value = [1, 2, 3, 4];
    println!("{:?}", value);
}

To get a pretty-printed output of an iterable data structure, you can use loops and a combination of print!() and println!().

fn main() {
    let values = &[1, 2, 3, 4, 5];

    for (idx, value) in values.iter().enumerate() {
        if idx != 0 {
            print!(", ");
        }
        print!("{}", value);
    }
    println!();
}

When convenient, use Debug printing instead.

fn main() {
    let values = &[1, 2, 3, 4, 5];

    println!();
}

This is supported for all types that implement the Debug trait. You can implement it for your own data types.

Comments and documentation

Distinguish between simple code comments and documentation comments that are used to generate HTML docs for your libraries.

// Comment

/// Documentation for the following item

//! Documentation for the enclosing item/file

Generate the documentation.

cargo doc