Getting Started

To install rust, please, follow the official documentation.

You should, at least, have the rust compiler (i.e., rustc) installed.

Managing Rust Projects with Cargo

Cargo Overview

cargo is Rust’s official build tool and package manager:

  • Simplifies project management, dependency handling, and building.

Usage examples:

\small

cargo new my_project    # create a new project
cargo build             # build project
cargo build --profile realease # builds optimized binary
cargo run               # runs the project
cargo test              # runs tests

Managing Rust Projects with Cargo

Benefits of Cargo

Dependency Management and Build Automation:

  • Automatically fetches and manages project dependencies.
  • Handles compilation, linking, and building release artifacts.

Project Structure:

  • Enforces a conventional project structure for consistency.

rustc: The Rust compiler.

  • Can be used directly, but Cargo simplifies many tasks.

Control Flow Constructs: If-statement

Rust provides flexible control flow constructs, including if statements, for-loops and while-loops.

  • The if statement syntax is straightforward:
let x = 5;
if x > 0 {
    println!("Positive!")
} else {
    println!("Non-positive")
}

Control Flow Constructs: Loops

While loop

while condition {
    // Code inside the loop
}

Example:

let mut n = 3;
while n > 0 {
    println!("Countdown: {}", n);
    n -= 1
}

Control Flow Constructs: Loops

For loop

for item in iterable {
    // Code to run for each item
}

Example:

let ns = vec![1, 2, 3, 4, 5, 6];
for n in ns {
    println!("Number: {}", n);
}

Ranges and Iterators

Rust has a powerful syntax to declare ranges over natural numbers:

  • Write 0..n to declare a range starting in $0$ to $n-1$, and
  • Write 0..=n to declare a range starting in $0$ to $n$

Example:

for n in 1..5 {
    // Code to run for each number in the range [1, 5[
}
for n in 1..=5 {
    // Code to run for each number in the range [1, 5]
}

Ranges and Iterators

Inspired from functional languages Rust allows us to write concise code using iterators. For example:

  • One can obtain the sum of a vector by simply writing:
let ns = vec![1, 2, 3, 4, 5];
let sum: i32 = ns.iter().sum();
  • Or, the squares of the previous vector with:
let squared = ns.iter().map(|x| x * x).collect();

Note that, |x| x * x represents a lambda/closure.

Closures

A lambda is an anonymous function used to express single operations or behaviours.

  • In Rust, lambdas are called closures
  • Closures are defined using the |param| expr syntax

In the previous example, |x| x * x is a closure that takes a single parameter x and returns the square of x (i.e., x * x). Typically, the use of closures is recommend in scenarios where a short, one-off function is needed, and there’s no need to define a separate function.

Pattern Matching

Rust also has a powerful pattern matching construct:

match value {
    pattern1 => // Code for pattern1,
    pattern2 if condition => // Code for pattern2,
    _ => // Default code,
}

For example:

let result = match x {
    1 => "One",
    2 | 3 => "Two or Three",
    x if x < 0 => "Negative",
    _ => "Other",
};

Option and Result

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

Option and Result are algebraic data types (ADTs) in Rust, providing a concise and explicit approach to managing potential missing values or errors in functions, thus eliminating implicit exceptions. This forces developers to actively acknowledge and handle these scenarios, promoting robust error management and contributing to enhanced code clarity and safety.

Option and Result

Examples:

let maybe_value: Option<i32> = Some(42);
let result: Result<i32, &str> = Ok(42);
// Unwrap value, if None returns the default the type
let value = maybe_value.unwrap_or_default();
// Multiply a valid result by 2
let result2 = result.map(|x| x * 2);

In Rust, it is recommend to use pattern matching to handle these types appropriately.

Parsing Integers from Stdin

Reading Input

Use std::io::stdin() to read input from the standard input:

let mut input = String::new();
std::io::stdin().read_line(&mut input)
    .expect("Failed to read line");

Parsing Integer from Stdin

Parsing Integers

Convert the input to a string using parse:

let parsed_num: Result<i32, _> = input.trim().parse();
match parsed_num {
    Ok(number) => // Use the parsed integer,
    Err(_) => // Handle parsing error,
}

Reliably Parsing Various Integers

Step 1

Create a helper function to parse integers from a string:

#[derive(Debug)]
struct ParserError(String);

fn usize(str: &str) -> Result<usize, ParserError> {
    str.trim()
        .parse::<usize>()
        .map_err(|e| ParserError(format!("{}", e)))
}

Note that, we don’t handle the error directly, we simply propagate it to the client using a special error ParserError.

Reliably Parsing Various Integers

Step 2

With a line from the stdin parse as many integers as you would like. Here, we assume integers are separated by a white space.

\scriptsize

fn pair() -> Result<(usize, usize), ParserError> {
    let mut buffer = String::new();
    std::io::stdin()
        .read_line(&mut buffer)
        .map_err(|e| ParserError(format!("{}", e)))?;
    let mut nums = buffer.split(" ");
    let fst = nums
        .next()
        .ok_or_else(|| ParserError(format!("fst")))
        .and_then(|s| usize(s))?;
    let snd = // same as above ...
    Ok((fst, snd))
}

Error Propagation

Try Operator

std::io::stdin()
    .read_line(&mut buffer)
    .map_err(|e| ParserError(format!("{}", e)))?; // <-

Note the special use of the ? operator in the previous code. The ? operator in Rust streamlines error propagation. Used in functions returning Result, it unwraps the value on Ok or short-circuits to return ParserError early. This simplifies error handling, reducing the need for explicit match or unwrap statements and contributing to concise, readable code.

Ownership in Rust

In Rust, each value has a variable that is its “owner”:

  • Ownership of a value can be transferred from one variable to another
  • The original variable can no longer be used
fn transfer_ownership(s: String) {
    // s is the owner of a String
    // Ownership transferred to this function
} // s goes out of scope, memory freed
let x = String::from("Hello");
// Ownership transferred to the function
transfer_ownership(x);
// println!("{}", x); // Error: x is no longer valid

Borrowing in Rust – Immutable Borrowing

Borrowing allows references to values without transferring ownership:

  • References can be mutable or immutable
  • Multiple references can borrow a value immutably:
    • No changes can be made

Example:

fn immutable_borrow(s: &String) {
    // s is an immutable reference to a String
}
let x = String::from("Hello");
immutable_borrow(&x); // Passing an immutable reference

Borrowing in Rust – Mutable Borrowing

Only one mutable reference is allowed at a time:

  • Prevents data races

Example:

fn mutable_borrow(s: &mut String) {
    // s is a mutable reference to a String
}
let mut x = String::from("Hello");
mutable_borrow(&mut x); // Passing a mutable reference

Heap Allocation in Rust

Rust allows dynamic allocation on the Heap. Ownership ensures memory safety without GC.

  • The Box type is used for heap allocating
  • Boxes have a single owner

\small

fn heap_allocation() -> Box<Vec<i32>> {
    // Allocating a Vec<i32> on the heap and returning a Box
    Box::new(vec![1, 2, 3])
}
// Ownership transferred to the variable data
let data = heap_allocation();

Heap Allocation in Rust – Dropping and Cleaning

Ownership rules govern cleanup; no need for explicit deallocation:

  • When a variable goes out of scope, its memory is automatically freed
{
    let data = Box::new(String::from("Heap Allocated"));
} // Memory is freed here

Practical Example – Parsing and Storing a Graph in the Heap

Step 1 - Choose a Memory Layout

To first parse a directed graph, $G$, choose how you’re going to represent it on memory, for example, using a adjacency list:

struct Graph {
    v: usize,
    adj: Vec<Vec<usize>>,
    rev_adj: Vec<Vec<usize>>,
}

In this struct we store:

  • v, the number of vertexes
  • adj, the adjacency list of the directed graph $G$
  • rev_adj, the reverse adjacency list of the graph, denoted $G^T$

Practical Example – Parsing and Storing a Graph in the Heap

Step 2 - Parse the Graph from the Stdin

  • Read the edges from stdin and store them in the adjacency list
  • Return a reference to a heap allocated graph

\scriptsize

fn parse_graph() -> Result<Box<Graph>, ParserError> {
    let (v, mut e) = pair()?;
    let mut adj: Vec<Vec<usize>> = vec![vec![]; v];
    let mut rev_adj: Vec<Vec<usize>> = vec![vec![]; v];
    while e > 0 {
        let (u, w) = pair()?;
        assert!((u - 1) < v);
        assert!((w - 1) < v);
        adj[u - 1].push(w - 1);
        rev_adj[w - 1].push(u - 1);
        e = e - 1
    }
    Ok(Box::new(Graph { v, adj, rev_adj }))
}

Practical Example – Parsing and Storing a Graph in the Heap

Step 3 - Entry point

The lifetime of the graph is only within its owner scope:

fn main() -> Result<(), ParserError> {
    // Parse the graph and obtain ownership
    let graph = parse_graph()?;

    // Perform operations with the graph
    // ...

    // Graph ownership goes out of scope, and
    // memory is automatically freed
    Ok(())
}

Tips: Rust’s Smart Referencing

Rust’s implicit reference coercion automatically converts certain types to references when needed, simplifying the use of owned values in contexts requiring references and eliminating explicit conversion syntax.

\scriptsize

fn process_graph(g: &Graph) {
    // Automatically coerces Box<Graph> to &Graph when passed
}
fn main() -> Result<(), ParserError> {
    // Parse the graph and box it on the heap
    let boxed_graph: Box<Graph> = parse_graph()?;
    // Pass the boxed graph to a function expecting a reference
    process_graph(&boxed_graph); // Implicit reference coercion
    Ok(())
}

Tips: Rust’s Smart Dereference

Rust’s smart dereference coercion simplifies the process of dereferencing, automatically converting certain types to references when needed. This eliminates the need for explicit dereferencing syntax.

\small

fn process_graph(g: &Graph) {
    // Automatically coerces &Graph to Graph when
    // dereferencing parameter v
    println!("Number of vertices: {}", g.v);
}

Tips: Efficient Code with Asserts

Leveraging asserts at the top of a function can optimize code execution by enabling the Rust compiler to eliminate runtime bounds checks when iterating through a vector data using indices.

\small

fn process(data: &Vec<i32>, size: usize) {
    assert!(data.len() >= size);

    for i in 0..size {
        // Internal vector bounds checks can be
        // optimized away because of the assert invariant
        data[i] ...
    }
}

Tips: Useful resources

Rust’s standard library: https://doc.rust-lang.org/std/

  • Indispensable

Rust by example: https://doc.rust-lang.org/rust-by-example/

  • Examples that illustrate various Rust concepts

The Rust Programming Language: https://doc.rust-lang.org/book/