Basics II
Let's continue with some more of the basics of Rust programming.
Options
Option
is a very important feature in Rust, it can assume either Some(value)
or None
. For instance, a variable with type Option<&str>
can assume values like None
, Some("hi")
, Some("hello")
, etc while a variable with type Option<i64>
can assume values like None
, Some(23)
, Some(100)
. None
is useful to signalize empty, invalid state, etc. Here are some examples:
let mut x = Some(3);
println!("{:?}", x);
// note that Some(3) and None have the same type
// as we don't create new variable here
x = None;
println!("{:?}", x);
// explicit typing is need here
let y: Option<i64> = None;
println!("{:?}", y);
// explicit typing not needed, check the types on
// Rust analyzer to see how Rust deduces them
let mut z = None;
z = Some("Hei");
println!("{:?}", z);
let v = vec![1, 2, 3];
// element exists, returns Some(value)
println!("{:?}", v.get(1));
// element doesn't exist, returns None
println!("{:?}", v.get(4));
Destructuring options
We can use pattern matching with if let
to destructure options:
let v = vec![1, 2, 3];
if let Some(val) = v.get(1) {
println!("We got a value: {}", val);
} else {
println!("We got None!");
}
if let Some(val) = v.get(4) {
println!("We got a value: {}", val);
} else {
println!("We got None!");
}
Another possibility is using match
:
let v = vec![1, 2, 3];
match v.get(1) {
Some(val) => println!("We got a value: {}", val),
_ => println!("We got None!"),
}
match v.get(3) {
Some(val) => println!("We got a value: {}", val),
_ => println!("We got None!"),
}
And yet another possibility it to use the while let
loop:
let mut v = vec![1, 2, 4, 3];
// pop() removes that last element x of the vector
// and returns Some(x)
// if the vector is empty, it returns None
while let Some(val) = v.pop() {
println!("We got a value: {}", val);
}
Finally, the ref
keyword is a somewhat forgotten but super useful feature of pattern matching, it doesn't affect the pattern matching rules, but if the pattern matches, then the value is borrowed as reference instead of taken ownership:
let val = Some("a string".to_owned());
if let Some(ref val) = val {
println!("We got a String: {:?}", val);
} else {
println!("We got a None");
}
// this will fail if you remove the ref above in the if let
println!("We got an Option: {:?}", val);
And similarly we have the ref mut
keyword to mutably borrow:
let mut val = Some("a string".to_owned());
if let Some(ref mut val) = val {
val.push_str(" that was processed")
}
// this will fail if you remove the ref mut above in the if let
println!("{:?}", val);
Which is particularly useful on loops:
let mut val = Some("hi".to_owned());
// every iteration of the loop mutably borrows val
// which means that if we removed the ref mut keyword
// this would fail to compile because it would try to
// take ownership on every iteration
while let Some(ref mut val) = val {
val.push('.');
if val.len() > 10 {
break;
}
println!("{:?}", val);
}
Results
Result
is a also very important feature in Rust, it can assume either Ok(value)
or Err(e)
. For instance, a variable with type Result<i64, String>
can assume values like Ok(83)
, Ok(103)
, Err("hi".to_owned())
, Err("hello".to_owned())
. Here are some examples:
let original = "Some string";
println!("original: {original}");
let bytes = original.as_bytes().to_vec();
println!("bytes: {:?}", bytes);
let reconstructed = String::from_utf8(bytes); // this is a Result
if let Ok(reconstructed) = reconstructed {
println!("reconstructed: {reconstructed}");
} else if let Err(error) = reconstructed {
println!("Failed to reconstruct with error: {error}");
}
// some invalid bytes as not every sequence of
// bytes creates a valid UTF-8 string
let bytes = vec![123, 154, 232];
println!("bytes: {:?}", bytes);
let reconstructed = String::from_utf8(bytes); // this is a Result
if let Ok(reconstructed) = reconstructed {
println!("reconstructed: {reconstructed}");
} else if let Err(error) = reconstructed {
println!("Failed to reconstruct with error: {error}");
}
let bytes = vec![123, 154, 232];
println!("bytes: {:?}", bytes);
let reconstructed = String::from_utf8(bytes); // this is a Result
let result = if let Ok(reconstructed) = reconstructed {
reconstructed
} else if let Err(error) = reconstructed {
error.to_string()
} else {
// this will never be reached but the compiler
// doesn't know, so we have this block
"should never be reached!".to_owned()
};
println!("{result}");
let bytes = vec![123, 154, 232];
println!("bytes: {:?}", bytes);
let reconstructed = String::from_utf8(bytes); // this is a Result
let result = if let Ok(reconstructed) = reconstructed {
reconstructed
} else {
"error while reconstructing".to_owned()
};
println!("{result}");
An again match
is also a possibility:
let bytes = vec![123, 154, 232];
println!("bytes: {:?}", bytes);
let reconstructed = String::from_utf8(bytes);
match reconstructed {
Ok(value) => println!("reconstructed: {}", value),
Err(error) => println!("error: {}", error),
}
Enums
Enums are types that can assume any one of several variants. In fact, we already saw two examples of enums: Option
and Result
, but we can define our own too:
enum CustomEnum {
A,
B(i32),
Multi(String, i64),
Named { x: i64, y: i64 },
}
fn parse_custom_enum(some_enum: CustomEnum) {
if let CustomEnum::A = some_enum {
println!("CustomEnum with variant A");
} else if let CustomEnum::B(v) = some_enum {
println!("CustomEnum with variant B with value {}", v);
} else if let CustomEnum::Multi(v1, v2) = some_enum {
println!("CustomEnum with variant Multi with values {} {}",
v1, v2);
} else if let CustomEnum::Named { x, y } = some_enum {
println!("CustomEnum with variant Multi with values x={} y={}",
x, y);
}
}
fn main() {
let some_enum = CustomEnum::A;
parse_custom_enum(some_enum);
let some_enum = CustomEnum::B(10);
parse_custom_enum(some_enum);
let some_enum = CustomEnum::Multi("hi there".to_owned(), 10);
parse_custom_enum(some_enum);
let some_enum = CustomEnum::Named { x: 10, y: 20 };
parse_custom_enum(some_enum);
}
Closures
Here's a few examples of using closures, anonymous functions that capture their environment, on Rust:
// example 1
// just a closure
let some_closure = || println!("Hello, closure world! 1");
some_closure();
// example 2
// closure captured a variable from outside
let x = 2;
let some_closure = || println!("Hello, closure world! {x}");
some_closure();
// example 3
// pass a variable as a parameter to the closure
let some_closure = |x| println!("Hello, closure world! {x}");
some_closure(3);
// example 4
// closure takes a variable by reference
// does not take ownership of the variable
let x = "Hello, closure world! 4".to_owned();
let some_closure = || println!("{x}");
some_closure();
// x was taken by reference by the closure
// so can it still be used!
println!("{x}");
// example 5
// closure takes a variable by mutable reference
// still does not take ownership of the variable
let mut x = "".to_owned();
let mut some_closure = || {
x = "Hello, closure world! 5".to_owned();
println!("{x}")
};
some_closure();
// x was taken by mutable reference by the closure
// so can it still be used!
println!("{x}");
// example 6
// closure takes a variable by value
// and therefore takes ownership of the variable
let x = "Hello, closure world! 6".to_owned();
let some_closure = || {
let y = x; // Takes ownership of x!
println!("{y}")
};
some_closure();
// x was taken by value by the closure, so CANNOT be used anymore!
// if you uncomment the line below, you will get a compiler error
// println!("{x}");
Easy options and results destructuring
Using closures and some builtin methods for options and results, common destructuring patterns became much easier and ergonomic:
let val = None;
let dval = val.unwrap_or_else(|| {
println!("Failed to get element, will return NAN");
std::f64::NAN
});
println!("dval = {}", dval);
let dval = val.unwrap_or(std::f64::NAN);
println!("dval = {}", dval);
// If the value is None, it will return
// the default value for the type, which
// for f64 is 0
let dval = val.unwrap_or_default();
println!("dval = {}", dval);
And similarly with Result
:
let bytes = vec![123, 154, 232];
let reconstructed = String::from_utf8(bytes);
let dstring = reconstructed.clone().unwrap_or_else(|err| {
println!("Failed to parse bytes to String with error: {}", err);
"invalid_utf8".to_owned()
});
println!("dstring = {}", dstring);
let dstring = reconstructed.clone().unwrap_or("invalid_utf8".to_owned());
println!("dstring = {}", dstring);
let dstring = reconstructed.unwrap_or_default();
println!("dstring = {}", dstring);
Another important one is map
:
let some_option = Some(20);
// If it's Some, map will take the value and apply it
// to the closure. If it's None, it will just keep
// the None and ignore the closure
println!("p1: {:?}", some_option.map(|v| v + 100));
let some_option: Option<i32> = None;
println!("p2: {:?}", some_option.map(|v| v + 100));
let some_result: Result<_, String> = Ok(20);
// If Ok, then apply the closure, if it's Err
// ignore the closure
println!("p3: {:?}", some_result.map(|v| v + 100));
let some_result: Result<_, String> = Ok(20);
// map_err, if Err, then apply the closure, if it's Ok
// ignore the closure
println!("p4: {:?}", some_result.map_err(|e| e + " there"));
let some_result: Result<i32, String> = Err("hello".to_owned());
println!("p5: {:?}", some_result.map(|v| v + 100));
let some_result: Result<i32, String> = Err("hello".to_owned());
println!("p6: {:?}", some_result.map_err(|e| e + " there"));
See the Result documentation and the Option documentation (very useful pages) for more details.
The ? operator
Consider the following:
// main() can also return a Result and
// if it returns Err, then the process
// will exit with error
fn main() -> Result<(), String> {
let mut try_val = Ok("Hello");
let val = match try_val {
Ok(val) => val,
Err(err) => return Err(err),
};
println!("First print: {val}");
try_val = Err("some error happened".to_string());
let val = match try_val {
Ok(val) => val,
Err(err) => return Err(err),
}; // will return here
// this won't be evaluated
println!("Second print: {val}");
Ok(())
}
The ? operator allows us to simplify this:
fn main() -> Result<(), String> {
let mut try_val = Ok("Hello");
let val = try_val?;
println!("First print: {val}");
try_val = Err("some error happened".to_string());
let val = try_val?; // will return here
// this won't be evaluated
println!("Second print: {val}");
Ok(())
}
The Err
type must be of the same type (or compatible) as the Err in the return type:
fn main() -> Result<(), String> {
let bytes = vec![123, 154, 232];
// try_reconstruct has type Result<String, FromUtf8Error>
let try_reconstructed = String::from_utf8(bytes);
// we cannot use the ? operator yet
// try_reconstruct now has type Result<String, String>
let try_reconstructed =
try_reconstructed.map_err(
|e| format!("Failed to parse bytes to utf8 {e}")
);
// now we can use the ? operator
// reconstructed now has type String
// or we return early with
// Err("Failed to parse bytes to utf8 {e}")
let reconstructed = try_reconstructed?;
println!("reconstructed: {}", reconstructed);
Ok(())
}
Moreover, using Box<dyn std::error::Error>
will allow the ?
operator to work on a broad range of compatible error types. And additionally, functions that return Option
can also take advantage of the ?
operator:
// Using Box<dyn std::error::Error will allow the ?
// operator to accept a broad range of error types
// see the
fn main() -> Result<(), Box<dyn std::error::Error>> {
let x: Result<i32, &str> = Ok(0);
x?;
let x: Result<(), String> = Ok(());
x?;
let mut x = vec![1];
x.try_reserve(10)?;
std::fs::File::open("some_random_file.txt")?;
Ok(())
}
fn multiply_first_10_values(myvec: &Vec<i32>) -> Option<i32> {
let mut result = 1;
for i in 0..10 {
let val = myvec.get(i)?;
result *= val;
}
Some(result)
}
Now as a fun game, try to understand what this one does:
fn main() -> Result<(), String> {
for i in 0.. {
// Unix epoch timestamp in microseconds
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.map_err(|e| format!("Error: {:?}", e))?
.as_micros();
let remainder_of_timestamp_divided_by_200 = timestamp % 200;
if remainder_of_timestamp_divided_by_200 == 0 {
println!("end");
break;
} else {
print!("{i} ");
}
}
Ok(())
}
Iterators
Rust has first class support for iterators and this gives the language a functional programming flavor:
let mut original_vec = vec![1, 2, 3, 4, 5];
// iter returns an iterator
// for_each takes a closure and applies it to each element
original_vec.iter().for_each(|x| print!("{} ", x));
// iter_mut returns an mutable iterator
original_vec.iter_mut().for_each(|x| *x += 10);
println!("{:?}", original_vec);
// map returns an iterator
let mapped_vec: Vec<_> = original_vec
.iter()
.map(|x| x + 100)
.collect();
println!("{:?}", mapped_vec);
// alternative: turbofish syntax
let mapped_vec = original_vec
.iter()
.map(|x| x + 100)
.collect::<Vec<_>>();
println!("{:?}", mapped_vec);
let filtered_vec = original_vec
.iter()
.filter(|x| **x < 14 && **x > 11)
.collect::<Vec<_>>();
println!("{:?}", filtered_vec);
// into_iter returns an iterator that takes
// ownership of the original vector
let mapped_vec: Vec<_> = original_vec
.into_iter()
.map(|x| x + 100)
.collect();
println!("{:?}", mapped_vec);
// original_vec is no longer available here!
For instance, the ubiquitous Python zip
and enumerate
are also available on Rust:
let x = [10, 70, 20, 100];
let y = vec![1, 8, 3, 1];
let iter = std::iter::zip(x, y);
let sum_pairs = iter.map(|(a, b)| a + b).collect::<Vec<_>>();
println!("{:?}", sum_pairs);
println!("{:?}", x.iter().enumerate().collect::<Vec<_>>());
Loops are also supported, of course:
let original_vec = vec![1, 2, 3, 4, 5];
for v in original_vec.iter() {
print!("{} ", v);
}
And you can turn strings into iterators, iterating over its UTF8 characters:
let some_string = "hi there!";
some_string.chars().for_each(|c| print!("{c} . "));
println!();
let some_string = "Слава Україні!";
some_string.chars().for_each(|c| print!("{c} . "));
println!();
Take a look at the Rust documentation regarding iterators.
Unwraps, expects, panics
Panic is a way to unwinding the stack and abort the current Rust program on error (actually it aborts only the thread, but here we are using single threaded programs). For instance, instead of destructuring Options and Results, you can call unwrap
on them and if they are holding a None
/Err
, they program will cause panic:
let bytes = vec![123, 154, 232];
// PANIC!
let reconstructed = String::from_utf8(bytes).unwrap();
let v = vec![1, 2, 3];
// PANIC!
let val = v.get(4).unwrap();
let v = vec![1, 2, 3];
// PANIC!
// expect is similar to unwrap, with the difference that
// it adds an extra text to the error message
let val = v.get(4).expect("Failed to get item from Vec");
In general, however, unwraps/expects are not desirable behavior and should be avoided in favor of destructing and the ? operator unless it's the initial prototype version of your program or for simple didactic code.
Note that unwrap_or_default
, unwrap_or
and unwrap_or_else
don't cause panics and are therefore recommended (see the Result documentation and the Option documentation for more details).
Hash maps
Hash maps are unordered key-value collections (similar to Python dict
). The key must have a single type across all elements and so must the values.
// This use statement is allows use of the HashMap type
// without the std::HashMap:: prefix on our code
use std::collections::HashMap;
fn main() {
let mut nationalities = HashMap::new();
nationalities.insert("John".to_string(), "American".to_string());
nationalities.insert("João".to_string(), "Brazilian".to_string());
nationalities.insert("Іван".to_string(), "Ukrainian".to_string());
nationalities.insert("John".to_string(), "British".to_string());
println!("print 0: {:?}", nationalities);
println!(
"print 1: {:?}",
nationalities.remove("Somebody who doesn't exist")
);
println!("print 2: {:?}", nationalities);
println!("print 3: {:?}", nationalities.get("John"));
println!("print 4: {:?}", nationalities.remove("John"));
println!("print 5: {:?}", nationalities.get("John"));
let mut other_hashmap = HashMap::new();
other_hashmap.insert(32, "some string".to_string());
println!("print 6: {:?}", other_hashmap);
let mut yet_other_hashmap = HashMap::new();
yet_other_hashmap.insert("some string".to_string(), 34);
println!("print 7: {:?}", yet_other_hashmap);
}
Hash sets
Hash sets are unordered lists without duplicates (similar to Python set
). The values must have a single type across all elements.
use std::collections::HashSet;
fn main() {
let mut names = HashSet::new();
names.insert("John".to_string());
names.insert("João".to_string());
names.insert("João".to_string());
names.insert("Іван".to_string());
println!("print 0: {:?}", names);
println!("print 1: {:?}", names.remove("Somebody"));
println!("print 2: {:?}", names);
println!("print 3: {:?}", names.contains("John"));
println!("print 4: {:?}", names.remove("John"));
println!("print 5: {:?}", names.remove("John"));
println!("print 6: {:?}", names.contains("John"));
}
Traits
Traits are a very important Rust feature, so here's an initial example:
#[derive(Debug)]
struct SomeStruct {
x: i32,
y: String,
z: f64,
}
trait SomeTrait {
fn print_x_plus_something(&self, v: i32);
fn add_to_x(&mut self, v: i32);
fn print_it(&self) {
println!("Default method");
}
}
// This block implements SomeTrait for SomeStruct
// (kind of obvious by the name, I know)
// not only this allow us to call the methods of
// SomeTrait for instances of SomeStruct but
// this has important implications for generic
// programming in Rust, as we will see shortly
impl SomeTrait for SomeStruct {
fn print_x_plus_something(&self, v: i32) {
println!("{:?}", self.x + v);
}
fn add_to_x(&mut self, v: i32) {
self.x += v;
}
// Uncomment the following lines and see what happens :)
// fn print_it(&self) {
// println!("{:?}", self);
// }
}
fn main() {
let mut s = SomeStruct {
x: 1,
y: "Hello".to_string(),
z: 3.54,
};
s.print_it();
s.print_x_plus_something(1000);
s.add_to_x(120);
s.print_it();
}
Impl Clone
Clone is an important Rust trait, so here's an example of how to implement it:
#[derive(Debug)]
struct SomeStruct {
x: i32,
y: String,
z: f64,
}
impl Clone for SomeStruct {
fn clone(&self) -> SomeStruct {
SomeStruct {
x: self.x,
y: self.y.clone(),
z: self.z,
}
}
}
fn main() {
let s1 = SomeStruct {
x: 1,
y: "Hello".to_string(),
z: 3.54,
};
println!("s1: {:?}", s1);
let mut s2 = s1.clone();
s2.x = 4;
println!("s2: {:?}", s2);
println!("s1: {:?}", s1);
}
However, there is an easy way: if all the types of the objects of the struct already implement Clone
individually, we can automatically derive the method clone for that struct (if you're not sure, you can just try it, and if some of types don't implement Clone, then the compiler will complain):
#[derive(Debug, Clone)]
struct SomeStruct {
x: i32,
y: String,
z: f64,
}
fn main() {
let s1 = SomeStruct {
x: 1,
y: "Hello".to_string(),
z: 3.54,
};
println!("s1: {:?}", s1);
let mut s2 = s1.clone();
s2.x = 4;
println!("s2: {:?}", s2);
println!("s1: {:?}", s1);
}
Generic functions
We can create generic functions in Rust that accept any type like this:
fn return_itself<T>(x: T) -> T {
x
}
fn main() {
let x = 10;
println!("{}", return_itself(x));
}
Now let's try to create a method that returns a clone (just a dumb wrapper around clone()
for didactic purposes):
// ERROR!
fn return_a_clone<T>(x: &T) -> T {
x.clone()
}
fn main() {
let x = "hi".to_owned();
let y = return_a_clone(&x);
println!("{}", x);
println!("{}", y);
}
The problem here is that a generic unspecified type T
might not have a method clone()
, but if we add a restriction that this T
has to implement the trait Clone
, then this will compile:
// it works!
fn return_a_clone<T: Clone>(x: &T) -> T {
x.clone()
}
fn main() {
let x = "hi".to_owned();
let y = return_a_clone(&x);
println!("{}", x);
println!("{}", y);
}
If we try to pass a argument that doesn't implement Clone
to return_a_clone
, then the compiler will complain (but not about the function, but about calling the function):
fn return_a_clone<T: Clone>(x: &T) -> T {
x.clone()
}
// uncomment to fix
// #[derive(Clone)]
struct SomeStruct(i64);
fn main() {
let x = SomeStruct(10);
let y = return_a_clone(&x); // ERROR!
}
Impl Copy
If the types of all elements of a struct implement Copy
, then the struct can implement Copy
to by using a derive
macro:
#[derive(Clone, Copy)]
struct SomeStruct {
x: f64,
y: f64,
}
fn return_a_copy<T: Copy>(x: T) -> T {
x
}
fn main() {
let s1 = SomeStruct { x: 3.54, y: 4.32 };
let s2 = s1;
let s3 = return_a_copy(s1);
println!("{} {}", s1.x, s1.y);
println!("{} {}", s2.x, s2.y);
println!("{} {}", s3.x, s3.y);
}
If you found this project helpful, please consider making a donation.