Why Learn Rust?

Thomas J. Kennedy

Contents:

1 Burying the Lead…

Most of the time… I have a habit of getting so enthralled with Rust discussions about safety guarantees, the Type-State Builder Pattern, the solution to reflection in testing, and mutabilty… that I forget to emphasize the motivation behind Rust.

Rust was designed to write safe concurrent (e.g., parallel) code quickly.

2 Cargo

According to the official documentation… Cargo is “the Rust package manager.”

Cargo is the Rust package manager. Cargo downloads your Rust package’s dependencies, compiles your packages, makes distributable packages, and uploads them to crates.io, the Rust community’s package registry. You can contribute to this book on GitHub.

Retrieved from https://doc.rust-lang.org/cargo/index.html on 02 January 2025

While true… Cargo serves as more than just a package manager. It handles:

Most other languages have analogous tools, e.g.,

However, cargo is developed alongside Rust. It is not an independent tool.

3 Procedural, Functional, & Object Oriented Programming

There are generally three styles of code found in codebases:

Some languages end up mixing all three (e.g., Python). Other languages are a little more interesting…

Rust code, especially code that uses iter, iter_mut, or par_it (from Rayon), is usually mostly functional in style.

3.1 Procedural

Rust written in a procedural style reads a lot like C++, Java, or Python code.

    let point1: (f64, f64) = (0.0, 5.0);
    let point2: (f64, f64) = (8.0, 3.0);
    let point3: (f64, f64) = (1.0, 7.0);

    let points = vec![point1, point2, point3];

    for point in points.iter() {
        let distance = ((point.0).powf(2.0) + (point.1).powf(2.0)).sqrt();
        println!("From (0, 0) to {:?} is {:4.2} (distance units)", point, distance);
    }

We should probably clean up this example a little…

    let points: Vec<(f64, f64)> = vec![(0.0, 5.0), (8.0, 3.0), (1.0, 7.0)];

    for point in points.iter() {
        let distance = ((point.0).powf(2.0) + (point.1).powf(2.0)).sqrt();
        println!("From (0, 0) to {:?} is {:4.2} (distance units)", point, distance);
    }

There is no reason to create each tuple as a separate variable… especially since each variable will be move-ed into the vector.

3.1.1 Rust Gotchas

If you come from a C++/Java/Python type background, you are probably familiar with pass-by-value (pass-by-copy) and pass-by-reference (pass-by-alias). Rust’s data ownership model replaces:

If you come from a C++/Java/Python type background, you probably think of variables as mutable first and read-only sometimes, e.g.,

int x = 7;
x += 32;

const int size_of_array = 12;

Rust flips this approach. Variables are read-only first… and mutable by request, e.g.,

let mut x: u64 = 7;
x += 32;

let size_of_array: usize = 12;

or

let x: u64 = 7;
let x: u64 = x + 32;

let size_of_array: usize = 12;

The latter example actually replaces x by redefining it (i.e., replaces, or shadows, the original x with a new variable by the same name)!

3.2 Functional

Most loop logic in Rust is handled with iter, iter_mut, par_iter, or par_iter_mut when possible.

    let points: Vec<(f64, f64)> = vec![(0.0, 5.0), (8.0, 3.0), (1.0, 7.0)];

    let distance_f = |point: &(f64, f64)| -> f64 {
        ((point.0).powf(2.0) + (point.1).powf(2.0)).sqrt()
    };

    let distances = points
        .iter()
        .map(|&pt| distance_f(&pt))
        .collect::<Vec<f64>>();

    let shortest_distance: f64 = *distances.iter().min_by_key(|c| OrderedFloat(**c)).unwrap();
    let largest_distance: f64 = *distances.iter().max_by_key(|c| OrderedFloat(**c)).unwrap();
    let average_distance: f64 = distances.iter().sum::<f64>() / (distances.len() as f64);

    println!("Min Distance: {:4.2}", shortest_distance);
    println!("Max Distance: {:4.2}", largest_distance);
    println!("Avg Distance: {:4.2}", average_distance);

Note… to compare f64-s with min_by_key and max_by_key… we need the ordered_float crate. Rust forces NaN and Inf to be handled explicitly.

3.3 Object Oriented

Rust has some object-oriented leanings (including structs).

pub struct Point {
    x: f64,
    y: f64,
}

However, Rust code makes heavy use of traits. Think of traits as Java interfaces, or C++20 concepts, but more fundamental (and well-defined).

We need to discuss the rules of a class checklist.

C++ Java Python 3 Rust
Default Constructor Default Constructor __init__ new() or Default trait
Copy Constructor Clone and/or Copy Constructor __deepcopy__ Clone trait
Destructor
finalize (deprecated/discouraged) __del__ Drop trait
Assignment Operator (=)
Accessors (Getters) Accessors (Getters) Accessors (@property) Accessors (Getters)
Mutators (Setters) Mutators (Setters) Setter (@attribute.setter) Mutators (setters)
Swap
Logical Equivalence Operator (==) equals __eq__ std::cmp::PartialEq trait
Less-Than / Comes-Before Operator (<) Comparable<T> interface __lt__ std::cmp::PartialOrd trait
std::hash (actual hashing) hashCode __hash__ std::hash::Hash trait
Stream Insertion Operator (<<) toString __str__ std::fmt::Display trait
__repr__ std::fmt::Debug trait
begin() and end() iterator __iter__ iter() and iter_mut()

As we discuss Rust… we will come across a few more traits, including: