Why Learn Rust?
Thomas J. Kennedy
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:
- Dependency management - downloading, compiling and linking dependencies
- Configuration management - development (debug) and release
- Test management - unit test and integration test compilation and execution
- Code coverage - through Tarpaulin
- Code linting, style checking, and analysis - through
cargo fmt
andcargo fix
- Compilation - with
cargo build
- Execution - with
cargo run
Most other languages have analogous tools, e.g.,
- Java has Gradle and Maven
- C++ has CMake and autotools
- Python has
tox
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:
- Procedural
- Functional
- Object Oriented
Some languages end up mixing all three (e.g., Python). Other languages are a little more interesting…
-
C++ code is usually written in an object oriented style. However, recent additions (e.g.,
std::function
,std
algorithms and ranges) have added functional patterns.using std::string; using std::vector; int main(int argc, char** argv) { vector<string> some_terms {"Hello", "world", "with", "for", "while", "int"}; vector<int> term_lengths; std::transform( some_terms.begin(), some_terms.end(), std::back_inserter(term_lengths), [](const string& t) -> int { return t.size(); } ); return 0; }
-
Java is usually written in an object oriented style. However, streams and lambda functions incorporate a more functional style.
import java.util.Arrays; import java.util.List; public class IntStreamDemo { public static void main(String... args) { List<String> some_terms = Arrays.asList( "Hello", "world", "with", "for", "while", "int" ); int[] term_lengths = some_terms.stream() .mapToInt(s -> s.length()) .toArray(); } }
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:
- pass-by-copy with move
- pass-by-reference with borrow
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 trait
s. Think of trait
s as Java interface
s, 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 trait
s, including: