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