César D. Velandia

Rust


Getting started

Install via script

# install
curl https://sh.rustup.rs -aSf | sh
source $HOME/.cargo/env
export PATH="$HOME/.cargo/bin:$PATH"

# check installation
cargo -V
rustc -V

Create a template project via command cargo new --bin hello-world or cargo init (existing repo)

hello-world/
├── Cargo.toml # manifest
└── src
    └── main.rs # source
# compile and run a binary or example
cargo run

# compile current package, binary => ./target/debug/hello-world
cargo build

# run tests
cargo test

# publish to registry crates.io
cargo publish

Variables

// let keyword creates a new variable
let x = 8;

// Use operator : to set type explicitly
let y: &str = "16";

// all variables immutable by default, unless mut keyword is used
let mut z = 32;
mut += 8;

// shadowing allows reusing variables names
let y: i16 = 16;

// only value-assigned variables can be printed
println!("{}", x);

Data types

Integer Types in Rust

Length	Signed	Unsigned
8-bit	i8	u8
16-bit	i16	u16
32-bit	i32	u32
64-bit	i64	u64
128-bit	i128	u128
arch	isize	usize

Floating-point Types, boolean and character types

let x = 2.0; // f64
let y: f32 = 3.0; // f32

Boolean: one byte size

let t:bool = true;
if t {println!("happy")} else {println!("sad")}

Character: single quote, useful functions is_numeric and is_alphabetic

let z = 'ℤ';
let heart_eyed_cat = '😻';
println!("{:?}, {:?}", z.is_numeric(), heart_eyed_cat.is_alphabetic());

Tuple: multiple types grouped, fixed sized.

let tup: (i32, f64, u8) = (500, 6.4, 1);
// Use pattern matching to destructure a tuple
let (x, y, z) = tup;

// tuple element accessed with an index
assert!(tup.0 == 500);
assert!(x == 500);

Array: same type, fixed size (at compile time).

// explicit elements, inferred type
let a = [1, 2, 3, 4, 5];

// explicit type
let b: [i32; 5] = [1, 2, 3, 4, 5];
assert_eq!(3, b[3]);

// all zeros, size 5, inferred type
let c = [0; 5];
assert_eq!(c, [0, 0, 0, 0, 0]);
assert_eq!(c.len(), 5);

//arrays can be sliced 
assert_eq!([2, 3, 4], &a[1..4]);

more on arrays & slices here

Strings: Usually refers to String and string slice str& types.

let black = "#000"; //&str
let white = String::from("#FFF"); //String
let blanc = white.as_ref(); //&str

assert!(black == blanc); //expression fails but is valid

String vs. &str

let mut r: String;

r = "red".to_string();
r = String::from("red");
r = "red".to_owned(); //clones the reference
r = format!("red {}", "sea"));
r = "Red color!".to_string().replace("Red", "Blue");
r = "rEd".to_lowercase();
let mut b: &str;

b = "blue";
b = &String::from("abc")[0..1];
b = "  hello there ".trim();

function into() will cast to left side expression type when possible:

b = "blue".into();
r = "red".into();

Control flow

  • if/else and else if
 // if/else
 if a > b {a} else {b}
 
 // else if
 if a % 2 == 0{
 	println!("divisible by 2");
 }else if a % 3 == 0{
 	println!("divisible by 3");
 }else{
 	println!("other");
 }
 
 // if in a let statement
 let number = if condition { 5 } else { 6 };
 
  • Another variant if let allows to match one case and ignore the rest:
if let days::Sunday = today{
	println!("Relax!");
}else{
	println!("Be productive!"); //not ignoring the rest
}

// as opposed to match (see below)
match today{
	days::Sunday => println!("Relax!"),
    _ => println!("!Be productive"),
}
  • Three types of loops in Rust: loop, while, for
// loop will cycle until explicitly told to stop

loop {
	x = y + z;
    break;
}

// add a return value after break when using let
let total = loop {
	count = count + 8;
    break count;
};

// while
while number < limit {
	number += 1;
}

// for loop with an iterator, e.g., array.iter()
for i in iterator{
	println!(i);
}

// in reverse and using a Range
for n in (1..4).rev() {
	println!(n);
}

match

Sorts using a expression through different arms of the structure. Used with primitive types, enum/option

match today {
    days::Monday => "Meetings",
    days::Tuesday => "Laundry",
    days::Wednesday => "Movies",
    days::Thursday => {
    	println!("almost Friday!");
        "Planning"
    },
    _ => () // any other value
}
let mario = Some("Mario"); //Option<&str>

match name { //match against an Option<&str> instance
    None => None,
    Some(name) => Some("Hello " + name), // use the value
}

Search for a match and returns Option<T>

Data structures

Struct

Simple custom data type. Good as a placeholder or template for structured data. Use update syntax to create templates with defaults and change some.

//C-like structure: named fields
struct ClassicPoint {
    x: f32,
    y: f32,
    z: f32
}

// tuple-like structure: unnamed fields (use index)
struct TuplePoint(f32, f32, f32);

// unit, placeholder like structure
#[derive(Debug)] // makes it printable
struct UnitStruct(); 

let origin: ClassicPoint = ClassicPoint{x: 0.3, y: 0.4, z: 0.1};
let end = TuplePoint(1.0, 4.0, 3.0);
let unit = UnitStruct();

// destructuring, or access via field name or index
let ClassicPoint{x: origin_x, y: origin_y, z: origin_z} = origin;

//update syntax, useful for updating large structs (templates)
let new_origin = Point { x: 5.2, ..origin };

println!("{} != {} != {}", origin.x, new_origin.x, end.0);

println!({:?}, unit);

Enum

Algebraic data types in Rust. Widely use via Options, pattern matching match, and the construct if let – you can put any kind of data inside an enum variant.

enum IpAddr{
    V4(u8, u8, u8, u8),
    V6(String),
}

enum Expr{
        Null,
        Add(i32, i32),
        Sub(i32, i32),
        Mul(i32, i32),
        Div {dividend: i32, divisor: i32},
        Val(i32),
}

let ipv4 = IpAddr::V4(127, 0, 0, 1);
let quotient = Expr::Div{dividend: 10, divisor: 2}; 
let sum = Expr::Add(40, 2);

And implement in the Enums & Structs using impl

impl IpAddr{
	fn print(&self){
    	println!("{:?}", &self);
    }
}

ipv4.print();

The Option Enum

Value encoded could be something or nothing so that compiler is able to check all cases. Replaces null references (nonexistent in Rust)  therefore can't be used directly and null cases must be managed explicitly. Assume only Option<T> may contain null values. The match control flow is used to deal with all cases .

// part of std, no need to include
enum Option<T> {
    Some(T),
    None,
}

let some_int: Option<i8> = Some(5);
let some_string = Some("wfh");
let none_float: Option<i32> = None; // for None; type must be explicit

let sum = 10 + some_int; //error! can't be used directly

Functions

pub fn final_price(price: i16, discount: i16) -> i16 {
	if (discount > price){
		// early return
 		return price;
	}

	price - discount
}

fn main() {
	assert_eq!(90, final_price(100, 10));
}

reference

values are moved by default instead of being copied, with exceptions. Use & to pass by reference instead, applicable to function parameters too.

   let p1 = Point {x: 1, y: 2};
   //let p2 = p1; //invalid, values moved to p2, can't be used via p1 (below)
   let p2 = &p1;
   println!("{}", p1.x);
   print_point(&p1); //usable as function parameter

fn print_point(point: &Point) {
        println!("x: {}, y: {}", point.x, point.y)
}

clone types


#[derive(Clone, Debug)]
struct Point {
        x: i32,
        y: i32,
}

let p2 = p1.clone();
   print_point_clone(p1.clone());
   println!("{}", p2.x);
   println!("{}", p1.x);

fn print_point_clone(point: Point) {
        println!("x: {}, y: {}", point.x, point.y)
}

Copy types

special basic types aren't moved, so can be copied without issues, implement Copy in a structure to avoid moving values. Copy requires Clone. Copy can only be derived for a type containing values that implement Copy.

let num1 = 42; //basic types like integer aren't moved
let num2 = num1; //so can be copied
println!("{}", num1);

#[derive(Clone, Copy, Debug)]
struct Point {
        x: i32,
        y: i32,
}

let p2 = p1 // will work as expected!

mutable references

all is by default immutable, to change values pass by reference and make mutable

fn inc_x(point: &mut Point){ 
        point.x +=1;
}

let mut p1 = Point {x: 1, y: 2};
inc_x(&mut p1);

methods

Add to custom types

impl Point {  //impl Type construct
        fn dist_from_origin(&self) -> f64{ //special parameter &self, instance called on
                let sum_of_squares = self.x.pow(2) + self.y.pow(2); //calling methods on simple types!
                (sum_of_squares as f64).sqrt() //cast the values using keyword as
        }

        fn translate(&mut self, dx: i32, dy: i32){ //here self is mutable
                self.x += dx;
                self.y += dy;
        }
}

constructors

new: not a constructor, common idiom, static method = associated function, doesn't take self, Self is the type of the self value (or use the custom type name, e.g., Point)

impl Point{       
	fn new(x: i32, y:i32) -> Self { 
                Self { x: x, y: y }
        }

        }

        fn start(x: i32, y:i32) -> Self {
                Self { x, y } //shorthand if value and field have same name
        }

        fn origin() -> Self {   //multiple constructors
                Point {x: 0, y: 0}
        }

}

   let new_point = Point::new(11, 22); // allocates values on the stack


Modules

mod sausage_factory {
    pub fn make_sausage() {
        println!("sausage!");
    }
}

fn main() {
    sausage_factory::make_sausage();
}

Macro

Define a macro and invoke withmy_macro!(args)

macro_rules! my_macro{
    ($val:expr) => {
      String::from("Super ") + $val  
    };
}

Tests

Use assertions to check statements (debug or release), triggers panic! macros.

let flag = is_enabled(x, y);

// panic message is string value of flag
assert(flag);

// using a custom panic message
assert(flag, "not enabled");
assert(flag, "not enabled: {}, {}", x, y);

writing a test

Use keywords #[cfg(test)] and #[test] to write your tests. Use assert, assert_eq, assert_neq or custom macros. Run using cargo test

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

    #[test]
    fn returns_twice_of_positive_numbers() {
        assert_eq!(double(4), 4 * 2);
    }

    #[test]
    fn returns_twice_of_negative_numbers() {
        assert_eq!(double(-4), -4 * 2);
    }
    
    #[test]
    fn test_my_macro() {
        assert_eq!(my_macro!("charge"), "Super charge");
    }    
}


Errors

Rust Compiler Error Index

Resources

  1. https://doc.rust-lang.org/stable/book/Most authorative reference with theory and runnable code samples
  2. https://doc.rust-lang.org/stable/rust-by-example/Exercises and code samples by topic
  3. https://rust-lang-nursery.github.io/rust-cookbook/Hands-on online book for common tasks and best practices
  4. https://users.rust-lang.org/ Official forum. Great place to find more learning resources
  5. https://github.com/rust-lang/rustlingsA starter collection of exercises with the basics and more!
  6. https://doc.rust-lang.org/nightly/nomicon/More advanced online book with unsafe code and low level details
  7. https://github.com/ctjhoa/rust-learningA bunch of links to blog posts, articles, videos, etc for learning Rust
  8. https://exercism.io/tracks/rustRust track to practice, compare solutions with others and get mentored
  9. https://stevedonovan.github.io/rust-gentle-intro/Introductory book filled with examples and concise explanations
  10. https://github.com/RalfJung/rust-101Alternative book with a different structure from Basic to Advanced
  11. https://towardsdatascience.com/you-want-to-learn-rust-but-you-dont-know-where-to-start-fc826402d5baComplete Free Resources for Rust Beginners
  12. https://github.com/brson/rust-anthology/blob/master/master-list.mdThis is a collection of substantial blog posts about Rust.
  13. https://github.com/rust-unofficial/awesome-rustA curated list of Rust code and resources.
  14. https://ferrous-systems.github.io/teaching-material/Teaching slides with concepts from basic to advanced
  15. https://github.com/pretzelhammer/rust-blog/blob/master/posts/learning-rust-in-2020.md#tldr
  16. https://github.com/pretzelhammer/rust-blog