Alessandro Dotti Contra


Some notes about Rust

Importing libraries

use std::io;

This will bring the io library (part of the std library) into the scope of the program.

Constants

const CONSTANT: u32 = 60;

Constants are immutable values, and their type must be annotated. They can only set to constant expressions.

Constants are valid for the entire life of the program, whithin the scope in which they are declared.

Variables

let myvar[: <type>] = <value>;
let mut myvar[: <type>] = <value>;

let keyword allows to define a variable, which will be immutable by default. The mut keyword is added to define a mutable variable. The type annotation is optional.

Note that is not possibile to mutate the type of a variable.

Variables can be shadowed.

Datatypes

Scalar types

Scalar types represent a single value. Supported scalar types are: intergers, floats, booleans and characters.

Numeric datatypes support the usual mathematical operators, while booleans supports the logical ones.

Integers

Can be signed (i8, i16 etc.) or unsigned (u8, u16 etc.).

Floats

Two floating-point number types are available: f32, f64.

Booleans

Variables of type bool has only two possibile values: true and false.

Characters

let c: char = 'c';

Character literals are specified with single quotes.

Compound types

Tuple

let mytuple: (type1, ...,  typeN) = (x1, ..., xN);
let (a, b, c) = mytuple;
let two = mytuple.1;
let myunit = ();

A tuple is a fixed length compound type which groups together values of different types.

Tuples can be destructured, and values can be accessed directly via the . operator.

An empty tuple is called unit (()).

Array

let myarray: [i32; <N>] = [x1, ..., xN];
let myval = myarray[3];

Just like tuples, but values must be of the same type. Values can be accessed directly with the [] operator.

To create an array with the same value for all elements:

let myarray: [i32; <N>] = [<val>; <N>];

References

let  myref = &<var>;
let  myref = &mut <var>;

The & operator returns a reference to <var>. References, like variables, are immutable by default; the mut keyword is needed to make the reference mutable (and the target variable needs to be mutable as well).

let  myval = *myref;

To access the value referenced by &, use the * (dereference) operator.

Method calls and automatic referencing/dereferencing

As a convenience, when a method is called on an object via the . operator, the compiler automatically performs the necessary referencing or dereferencing as needed. It is thus possible to always write object.method() and avoid (&object).method() or (*object).method().

Slice

let myslice: &[T] = &mycollection[start..end];

Slices allow referencing a contiguous part of a collection, from index start to index end (excluded). start index can be omitted if it references the first element of the collection, and end index can be omitted if it references the last element of the collection. If both indexes are omitted, the slice refers to the whole collection.

T is the type of elements in the collection. In case of a slice of a string, use &str instead.

Ownership

The following rules are always true:

  • each value in Rust has an owner;
  • there can only be one owner at a time;
  • when the owner goes out of scope, the value will be dropped.

Moving data and passing ownership

In case of datatypes which size is not known at compile time, when a variable is assigned (=) to another variable, the value of the first variable gets moved and it's ownership passed to the new variable.

In case of datatypes which size is known at compile time, the assignment gets the value of first variable copied to the second (and no change of ownership is involved).

Cloning variables

The .clone() method (if available for the specific datatype) can be used to actually clone a variable. No change of ownership is involved as a new variable will be created.

Ownership and functions

The same pattern helds true when variables are passed as arguments to a function.

Returning values can also transfer ownership.

References and borrowing

References are represented by & and allows to refer a value without taking ownership of it. A reference borrows the value it refers.

References are immutable by default (the value they refer cannot be changed).

Mutable references can be defined with the keyword &mut (and the value they refer must be declared as mutable as well).

The following rules are enforced:

  • many immutabile references to an object are permitted allowed;
  • only one mutable reference to an object is allowed at any given time;
  • mutable and immutable references to an object in the same scope are not allowed.

Functions

fn myfunction(<paramenters>) [-> <type>] {
    ...
    [expression]
}

A function is declared with the fn keyword.

In the parameters list, the type of each parameter must be declared.

When a function returns a value, it's type must be declared in the function signature. Beside explicitly returning a value with the return keyword, a function can implicitly return the value of the last expression (a statement not terminated by a ; which returns a value).

The main function

fn main() {
    ...
}

The main function is the entry point of the program.

Output

println!(<string>[, var1, ..., varN]);

println! is a macro that prints a string to the standard output stream. It supports placeholders ({}, which in turn support optional formatting directives) to output variable values.

There is also the eprintln! macro, which prints a string to standard error stream.

Input

std::io::stdin()

The stdin() of the std::io module returns an instance of std::io::Stdin, which gives access to the standard input of the terminal.

Conditionals

If

if <expression> {
    ...
} else {
    ...
}

If statement is used to branch code: if the expression evaluates to true the first block is executed, otherwise the second. The else statement is optional.

Since an else if statement is available, it is possibile to handle multiple conditions.

let myvar = if condition { <val1> } else { <val2> };

If statement can also be used on the right side of a let statement.

Match

match <expression> {
    <pattern> => <action>,
    ...
    <pattern> => <action>,
    _ => <default action>,
}

The match statement compares the result of an expression against one or more patterns; the action associated to the first matching pattern is then executed. Note that all possibile cases must be handled; the _ placeholder can be used to execute a default action (or, by setting its action to (), to do nothing).

The action is an expression, and its value is what gets returned for the match expression.

If let

if let <pattern> = <expression> {
    ...;
}

The if let construct is a concise replacement for match, which executes code when the expression matches exactly one pattern and ignores all other cases.

It is possible to add an else clause to an if let construct.

Let else

let <pattern> = <expression> else {
      ...;
  };

The let else construct works the same way if else does, but it handles the case of pattern not matching the expression. If the pattern matches the expression, let else just returns the value binded to the pattern (if any) to the outer scope.

Loops

loop {
    ...
}

The loop keyowrd creates an infinite loop. The exit an infinite loop use the break keyword; to skip to the next loop iteration use the continue keyword.

Loops can return a value by adding it after the break keyword.

Nested loops

outer_loop: loop {
    ...
    inner_loop: loop {
        ...
        break;
        ...
        break outer_loop;
        ...
    }
}

In case of multiple, nested loops, since break and continue operates on the innermost loop, it is possible to label loops to achieve a finer grade control.

Conditional loops

while <expression> {
    ...
}

The while keyword allows to run a loop until the expression holds true.

Looping through collections

for <item> in <collection> {
    ...
}

The for keyword is useful for looping through a collection of items. To use a range in place of the collection simply replace it with the n..m operator (which generates a range from n to m excluded).

Structures

Definition and instantiation

struct User {
    active: bool,
    username: String,
    group: String,
}

Structures can hold information of different types, with a label naming each piece of information (which is referred as field).

let alexandros = User {
    active: true,
    username: String::from("alexandros"),
    group: String::from("developers"),
};

When creating an instance of a structure, fields can be given value in any order.

Fields of a struct can be accessed with the . operator.

Structs support field init shorthand syntax: when a parameter name is exactly the same as the field label, the field label can be omitted.

Struct update syntax

let pericles = User {
    username: String::from("pericles"),
    ..alexandros
};

When an instance of a structure is derived from another instance of the same structure, it is possibile to value only the pieces of information that differ between the two instances, and use the .. operator to fill the remaining information automatically.

Note: since values are moved from the old instance to the new instance, if the type of any of the values moved does not implement the copy trait the old instance is no longer valid.

Destructuring

let User { active, username, group } = alexandros;

The let operator allows the destructuring of a struct the same way it allows the destructuring of a tuple.

Tuple structs

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

Tuple structs are tupes with a type associated, without the need to use a label for each field.

Unit like structures

struct Meaningless;

Unit like structures are structures without fields; they are useful to define a trait on a data type without storing any actual value.

Printing debug information

#[derive(Debug)]
struct MyStruct {
    ...
}

By annotating the structure with #[derive(Debug)], it possible to use the debug formatter to print out the fields of the structure:

let s = MyStruct { ... };
println!("Inspecting structure: {s:#?}");

Methods and associated functions

impl MyStruct {
    fn a_method(&self) -> <type> {
        ...
    }
}

Methods are just functions, but defined in the context of a structure (that's what the impl block stands for). The also have self as their first parameter (which represent the instance of the structure the method is called on).

Methods are called associated functions, because their associated with a specific type. It is possible to define associated functions which are not methods (the don't require self to be the first parameter). This kind of associated functions are called with the :: operator (since they are not bind to any instance). Es: String::from().

Enums

Definition

enum MyEnum {
    val1,
    ...
    valN,
}
let value = MyEnum::valN;

Enum allows to have a value among a finite set of values; they define a type (just like structures do).

enum MyEnum {
    val1(type1, ..., typeN),
    ...,
    valN(type1, ..., typeN),
}
let value = MyEnum::valN(val1, ..., valN);

Each variant of an enum can have different types and amount of data associated.

Methods

It is possible to define methods on enums (again, just like structures allow).

Option type

  enum Option<T> {
    None,
    Some(T),
}

The Option<T> type (where <T> identifies a generic type) is an enum which implements the concept of a variable being either None or having Some actual value. Each different <T> defines a different Option type.

Match construct

enum People {
    Alice,
    Bob,
    Ludwig,
};

fn find_age(person: People) -> u8 {
    match person {
        People::Alice => 21,
        People::Bob => 34,
        People::Ludwig => 90,
    }
}

The match expression can use enum values as patterns.

If an enum variant has some value associated to it, the value can be binded like so:

MyEnum::Variant(value) => {
    ...;
}

value can be then used inside the action associated to the matching pattern.

The same logic is valid for Option<T> types as well: bind the <T> data associated to the Some variant to an actual value and use it.

Modules

Declaration

mod my_module;

A module can be declared with the mod keyword in the root file (main.rs or lib.rs) of a crate. It can be defined in src/my_module.rs file. The code within a module is private by default, unless the module is declared with the pub mod keyword. The pub keyword must also be used for any item inside the module intended to be public.

If a module is declared in any file but the root file of a crate, it is treated as a submodule.

Using modules

The use keyword brings a public module into scope. The path to the module can be either relative (starting with self::, super:: or an indentifier in the current module) or absolute (with the keyword crate:: referring to the current module).

Aliasing is achieved with use <module> as <alias>.

use <path>::<to>::<module>::{item1, ..., itemN);

To bring multiple items from the same path, nested paths are supported.

Structs and Enums

Using the pub keyword before a struct definitiion makes the structure public but the struct fields will still be private; each field designed for public access must have a preceding pub keyword as well.

Using the pub keyword before an enum definition makes the enume and its variants public.

Common collections

Vectors

let v: Vec<i32> = Vec::new();
let w: Vec<i32> = vec![1, 2, 3];

Vectors (Vec<T>) allow to store values of the same type in a single data structure. The vec! macro creates a new vector filling it with the values provided.

Elements are inserted into a vector with the push() method and removed (and returned) with the pop() method.

let two: &i32 = &v[1];
let two: Option<&i32> = v.get(1);

Elements can be read using indexes, or via the get() method. The get() method returns an Option<&T> value which can be then used in a match statement.

Note: due to the fact that the elements of a vector are stored side by side in memory, it is not possibile to have a mutable vector and a reference to any of its elements.

To iterate over the elements of a vector:

for i in &v { ... }     // Iteration over immutable values
for i in &mut v { ... } // Iteration over mutable values

Strings

let mut s = String::new();
let s = String::from("Hello World");

Strings are implemented as a collection of bytes and are UTF-8 encoded.

Any type which implements the Display trait can be converted to a string with the to_string() method.

Strings can grow in size and can be concatenated with the + operator or the format! macro. The push_str() method allows to append a string slice to an existing string (push() to append just a character).

To iterate over a string, use the chars() method to obtain the single characters the string is made of.

Hash Maps

use std::collections::HashMap;

let mut colors = HashMap::new();

colors.insert(String::from("Red"), 0xFF0000);
colors.insert(String::from("Blue"), 0x0000FF);

The type HashMap<K, V> maps keys of type K to values of type V. All keys must be of the same type, and all values must be of the same type.

Values can be accessed providing the element key to the the get() method. The method returns a value of type Option<&V>.

for (key, value) in &colors {
    ...
}

Iteration over a hash map can be performed with the usual for loop construct. Iteration happens in an arbitrary order.

It terms of ownership, elements which type implements the Copy trait are copied into the hash map, otherwise the hash map becomes the owner of those elements.

colors.insert(String::from("White"), 0x000000);

// Overwrite the old value
colors.insert(String::from("White"), 0xFFFFFF);

// Set value only if the key does not exits
colors.entry(String::from("White")).or_insert(0xFFFFFF);

Error handling

panic!(<message>);

The panic! macro causes the program to exit immeditely, displaying the message provided as argument.

Recoverable errors

enum Result<T, E> {
    Ok(T),
    Err(E),
}

The Result enum is available for handling those errors that can be handled, rather than let the code panic. The type T is the type of object returned in case of success, while E describes the error occurred.

The .unwrap() method of the Result enum returns the value inside the Ok, otherwise will call panic! in case of error.

The .expect() method does the same, but allows for a customized message in case of panic.

Errors propagation

fn open_myfile() -> Result<std::fs::File, io::Error> {
  let mut myfile = File::open("myfile.txt")?;
  Ok(file)
}

The ? allows to handle error propagation. If the function (which must return a Result<T, E>) succeeds, the value inside Ok() is returned, otherwise Err() will be returned to the calling code.

Note that the function inside which the ? is used must return the same type that ? will handle. Possibile types are Result<T, E> and Option<T>.

Generic data types

In function definitions

fn my_function<T>( ... ) -> ...

In a function definition, T specifies that the function will operate over a generic type. T is thus the placeholder for such generic type, and can be used in the parameters list and as a returning type as well, if needed.

If T needs to implement specific trait(s), add a where T: <trait1> + ... + <traitN> at the end of the function signature.

In struct definitions

struct myStruct<T> {
    a: T,
    b: T,
}

In struct definition, the placeholder between <> represents the generic type the struct will use. It is possible to specify the use of different generic data types by adding more placeholders between <>.

In enum definitions

enum Option<T> {
    Some(T),
    None,
}
enum Result<T, E> {
    Ok(T),
    Err(E),
}

Generic data types can be used in enum definitions the same way they are used in struct definitions.

In method definitions

struct myStruct<T> {
    a: <T>,
    b: <T>,
}
impl<T> myStruct<T> {
    fn a(&self) -> &T {
        &self.a
    }
}

By specifying T after impl, the method will be defined on any instance of the type, regardless the concrete type actually used. It is possibile to define a method only on a specific type by omitting the <T> specification after impl and explicit the type between <> in myStruct<...>.

Traits

Definition

pub trait myTrait {
    fn myFunction(&self, <parameters>) -> <return type>;
}

A trait defines a behaviour for a data type, and consists of methods which can be called on that type. If a data type wants to implement a trait, it needs to provide its custom behaviour for all of such methods.

Implementation

impl myTrait for myType {
    fn myFunction(&self, <parameters>) -> <return type> {
        ...
    }
}

Once implemented, traits must be brought into scope with use keyword.

A trait on a type can be implemented only if either the type or the trait (or both) are local to the crate.

Default implementation

If, in the trait definition, methods are implement rather than just defined, that implementation will be used as a default for all data types whishing to implement the trait.

impl myTrait for myType {}

Each data type can the use this default, or override it.

Traits as parameters

fn func<T: myTrait>(<parameters>) -> <return type> {
    ...
}
fn func<T: myTrait + myOtherTrait>(<parameters>) -> <return type> {
    ...
}

To specify that a parameter can be of any type that implements a trait, the <> syntax is extended by including, alongside the type placeholder, the trait(s) required.

fn func<T, U>(<parameters>) -> <return type>
where
T: myTrait + myOtherTrait,
U: myTrait + anotherTrait,
{
    ...
}

The where clause is available for those cases with multiple generic types and complex trait requirements.

Returning types

fn myfunc(<parameters>) -> impl myTrait {
    ...
}

It is possibile to define a function that returns a generic type that must implement a trait. The function must return a single type.

Conditionally implement methods

It is possible to implement different methods for impl blocks that use generic types, by limiting the block to those types which implements one or more traits.

Lifetimes

Lifetimes in functions

fn myfunction<'a>(x: &'a i32, y: &'a i32) -> &'a i32 {
    ...
}

Lifetimes describe the relationship between the lifetime of the references involved. They do not affect the lifetime of the reference.

Lifetimes in structs

struct myStruct<'a> {
    text: &'a str,
}

Lifetimes in methods

impl<'a> myStruct<'a> {
    ...
}

Static Lifetime

The 'static lifetime denotes that the affected reference can live for the entire duration of the program.

Tests

Test functions

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn my_test() {
        ...
    }
}

Test functions are part of a tests module and annoted with #[test] just before their definition.

Test functions are run in parallel (unless cargo is instructed otherwise).

The output of successful tests is captured and thus not shown (cargo test has an option to change this behaviour).

The annotation #[ignore] marks a test for skipping when running the test suite.

Available macros

The assert! evaluates a boolean. If the boolean is true, nothing happens, otherwise the macro calls panic! and the test fails.

The assert_eq! and assert_ne! test for equality and inequality between two arguments. The arguments tested need to implement the PartialEq and Debug traits.

The #[should_panic] annotation checks if a function actually panics. It is possible to provide and expected argument to better intercept the reason for the panic.

Closures

// let inc = |x: usize| -> usize { x + 1 };
let inc = |x| { x + 1 };
println!("{}", inc(5));

Closures are anonymous functions that can be saved to a variable or passes as argument to other functions. They can capture values from the scope in which ther are defined. Type annotations for parameters and return value are optional.

Fn traits

Closures implements one or more of the following traits in an additive fashion:

  • FnOnce: all closures implement this trait since the can be called;
  • FnMut: implemented by closures that might mutate the captured values;
  • Fn: implemented by closures that do not capture any value or do nothing with the values they'd captured.

Iterators

let v = vec!['a', 'b', 'c'];
let v_iterator = v.iter();
for i in v_iterator {
    println!("Item {i}");
}

Iterators iterate over a sequence of items and determine when the sequence has finished.

Iterators implement the Iterator trait and a next() method. They are lazy, and need to be consumed by calling some specific method (many methods are implemented in the Iterator trait by the standard library).

Smart pointers

Smart pointers are data structures that act like pointers, but can have more capabilities. Such data structures must implement the deref trait (to access the data pointed to by the smart pointer) and, optionally, the drop trait (which acts as a destructor and is called when an instance of the smart pointer goes out of scope).

Concurrency

Threads

use std::tread;

thread::spawn(|| {
    ...;
});

The thread::spawn allow the creation of a new thread. Note that, when the main thread ends, all spawned threads are shut down.

To wait for all the treads to finish, save the result of thread::spawn and then call the join() method on that result; this will block the thread in which that call is performed until the spawned thread(s) are finished.

thread::spawn(move || {
    ...;
});

Using the move keyword allows the closure passed as argument to the spawn() method to take ownership of any variable (from the calling environment) it may reference.

Channels

use std::sync::mpsc;

fn main() {
    let (tx, rx) = mpsc::channel();
}

Channels are a way for threads to communicate by exchanging information. mpsc::channel() creates a multiple producer, single consumer type of channel.

When a value is sent down a channel, the ownership is transferred from the producer to sending thread to the receiving thread.

Mutexes

Mutexes grants exclusive data access to a thread.

use std::sync::Mutex;

fn main() {
    let m = Mutex::new(5);
    ...;
}

The Mutex<T> type implements a mutex. Before accessing its data, use the lock() method to acquire the (exclusive) lock on the mutex.

Asyncronous programming

Futures and Async

Futures are values that will become available at some time in the future. Any data type that implements the Future trait can be a future.

The aync keyword marks a block or a function which can be interrupted and resumed as needed. Whithin that block or function, the await keyword is used to wait for a future value.

Runtimes

To run async blocks of code, a runtime is needed to control and supervise the status of those blocks.

The control is passed to the runtime when an await keyword is encountered.

Trait objects

A trait object is a pointer to both an instance of a type implementing that particular trait, and a table used to look up methods of that type at runtime. It can be used in place of a generic or concrete type, and its purpose is to abstract a common behaviour.

To define a trait object use the dyn keyword before the trait we want to be implemented.