Rust CheatSheet

Table of Contents

Table of Contents

1. Hello World!

fn main() {
    println!("Hello, world!");
}

Compile and run this with
rustc hello_world.rs; ./hello_world

The main function is special: It is always the first code that runs in every program. For now, know that names ending in ! are “macros” —special functions that transform code.

1.1. cargo new

Packages of code are referred to as crates —which are shipped with cargo, the build tool and package manager. Cargo is the most convenient way to create a new Rust project.

Running cargo new my-first-program creates a folder my-first-program with a bunch of stuff in it. Importantly, it has a src/main.rs file which is our entry point (which prints ‘Hello, world!’) and Cargo.toml which is a configurations file —e.g., to keep track of what external packages our program requires. We can now do cd my-first-program; cargo run to compile and run our program in one go: Hello, world! is printed.

To install a new crate, simply add it to the end of the [dependencies] list in Cargo.toml. For example, open Cargo.toml and at the very end, add rand = "0.8.5". Next time you do cargo run it will install the rng randomness package.

1.2. Let's see a full program viz a Number Guessing Game

use std::io;   // Use the standard input-output library
use rand::Rng; // Use Random Number Generator trait, needs “rand” crate
use std::cmp::Ordering; // Enumeration to denote result of comparisons

fn main() {
    println!("Guess the number!");
    // Ranges “m..n” are all integers i with m ≤ i < n.
    let secret_number = rand::thread_rng().gen_range(1..101);

    // The “loop” keyword creates an infinite loop.
    loop {
        println!("Please input your guess.");

        // “mut”able variable, bound to an empty String object
        let mut guess = String::new();

        // Get a handle to the standard input for your terminal.
        // Then, stick user input into (a mutable reference to) “guess”.
        // If something goes wrong, show an informative message.
        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        // Instead of “expect”, we can use “match” to move from
        // crashing on an error to instead handling the error...

        /* Shadow the “guess” variable to avoid using a new variable name.

           Handle the possibility of parse errors using Pattern Matching:
           We try to make an unsigned integer out of the user's string input;
           if no number is parsed, we ask the user to guess again. */
        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_)  => continue // Restarts the loop
        };
        // Alternatively: guess.trim().parse().unwrap() tries to get “num”; otherwise it crashes.
        // “unwrap” and “expect” are useful for quick development, but otherwise dangerous to use.

        match guess.cmp(&secret_number) {
            Ordering::Less    => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal   => { println!("You win!"); break }, // Exit the loop
        }
    }
}

Shadowing (overriding) let's us reuse names: Mutable variables can change their value only, whereas shadowing means we can change the value and the type. Shadowing is done with let, whereas mutable variables can be assigned to directly.

  • With shadowing, let x = f(x), we can perform a few transformations on a value but have the variable be immutable after those transformations have been completed.

The parse function returns a value of type Option, which might contain something or it might not contain anything. (Akin to type-safe nullables).

Exercise! Instead of the continue on parse errors, use std:process library to quit the program with process::exit(1) after providing a useful message to the user.

Exercise! Instead of the continue on parse errors, quit the program with panic!("Not a number") —this violently stops execution with an error message, source file name, and line number. (Likewise, unwrap panics where there is no Option value to retrieve.)

Exercise! Notice that the first guess variable had its string type inferred. If we remove the explicit numeric type annotation from the second guess then parse wouldn't know what kind of value is being parsed and we get a compile error. Remove the explicit type annotation by specialising parse to get numeric u32 values using the so-called “turbofixh syntax”: parse::<u32>() .

2. Formatted text: format!, println!, dbg!

We can write formatted text to a String object using format! —or print it to the console instead using println!; both work the same and stringify their arguments.

Let's print 4 weeks and 31 days a few different ways:

fn main() {
    // Placeholders `{}` will be replaced with arguments.
    println!("{} weeks and {} days", 4, 31);
    // Or using “format!” first.
    let it = format!("{} weeks and {} days", 4, 31);
    println!("{}", it);

    // Positional placeholders can be used.
    println!("{1} weeks and {0} days", 31, 4);

    // As can named arguments.
    println!("{all} weeks and {every} days", every = 31, all = 4);

    // We can mix and match.
    println!("{:?} weeks {1:?} {all} days", 4, "and", all = 31);

    // We can also “pretty print” expressions; useful for lots of data
    println!("{:#?}", ["4", "weeks", "and", "31", "days"]);

    // We can see an expression, its value, and its location in code.
    dbg!(it); // Prints: [src/main.rs:19] it = "4 weeks and 31 days"
    let n = dbg!(1 * 2 + 3) - 1; // Prints: [src/main.rs:20] 1 * 2 + 3 = 5
    assert!(n == 4);

    // Sometimes it may be useful to see the memory address of a variable.
    println!("{:p}", &n);
}

Notice that the {:?} placeholder prints text for debugging purposes; e.g., strings keep their quotes. Alternatively, the dbg! macro takes ownership of an expression, prints the file and line number of where that dbg! call occurs along with the resulting value of that expression, and returns ownership of the value —this means that almost anywhere there is an expression 𝒳, we can just replace it with dbg!(𝒳) to get some useful debugging information about it! The dbg! macro can be really helpful when you’re trying to figure out what your code is doing!

  • If you're doing lots of println! just to see values of variables/expressions, just use dbg! instead: println!("x = {}, y = {}", x, y) can be replaced by dbg!(x, y).

3. The “::” Notation: Types as Namespaces and General Imports

Dots are generally used to get field from an object such as p.0 for pairs; the double-colon is used similarly but for namespaces as in library::file::function.

fn main() {
    assert!(std::cmp::min(3, 8) == 3);
}
  • use directives can be used to “bring into scope” names from other namespaces.

    // use std::cmp::min;        // Single import
    // use std::cmp::{max, min}; // Multiple imports
    use std::cmp::*;             // Imports everything
    fn main() {
        assert!(min(5, 5) == max(5, 5));
    }
    
  • Types are namespaces too, and methods can be called as regular functions:

    fn main() {
        assert!("hola".len() == str::len("hola"));
    }
    

    In-general, object.method(args) ≡ type::method(object, args).

4. A First look at Ownership: Why let x = ⋯; f(x); f(x) crashes

Many languages distinguish between ‘literals’ and ‘objects’, Rust does so with a feature called ownership, which aims to prevent us from writing memory unsafe code.

The following program looks like it should work without any problems…

 1: fn main() {
 2:     let mut hi = "hello".to_string(); // We want an object; not a literal primitive.
 3: 
 4:     fn speak(words: String) {
 5:         println!("{}", words)
 6:     }
 7: 
 8:     speak(hi);
 9:     // speak(hi); // Whoops, the *value* of “me” has moved out of this *variable*!
10: 
11:     hi = "yup".to_string();
12:     speak(hi)
13: }

Ownership means variables own their values:

  • Line 2: The variable hi owns the string value "hello."
  • Line 8: The value "hello" is passed into the function speak, and so ownership of the value has moved. In-particular, hi no longer owns a value and so cannot be accessed after this line.
  • Line 9: We try to call the function speak with a variable that has no value and so we get a compile-time error.
  • Line 11: We can fix this by letting the variable hi own a value, then we can use it again.

If we do need access to a variable's value in multiple function calls, we will have to pass it by reference instead of by value.

 1: fn main() {
 2:     let mut hi = "hello".to_string();
 3: 
 4:     fn speak(words: &String) {
 5:         println!("{}", words)
 6:     }
 7: 
 8:     speak(&hi);  // Works first time.
 9:     speak(&hi);  // Works second time!
10: }

Borrowing means temporarily owning something: Line 4 says this function wants a reference to a string value; then line 8 calls the function by making a reference using the “borrow operator” &.

  • A reference is a type that references another value in memory.
    • They're useful when we don't want to pass “ownership” to control structures (such as for-loops) or functions.

  • A variable binding can be immutably borrowed multiple times:

    • 🥳 let x = 1; let ref1 = &x; let ref2 = &x.
  • While borrowed, a variable binding cannot be mutated:

    • 😦 let mut x = 1; let ref = &x; x = 2
  • While immutably borrowed, a variable cannot be mutably borrowed:

    • 😦 let mut x = 1; let ref1 = &x; let ref2 = &mut x.

Summary: Rust provides two variants of most types, those that can be owned and those that cannot. Unowned types are passed-by-value, or copied. Loosely put, this is the difference between literals and objects.

fn main() {
    // Literals have no ownership, so the following works fine.
    let x: &str = "hello";
    let y = x;
    dbg!(x, y); // ⇒ "hello", "hello"

    // Objects have a single owner, so the following crashes.
    let x = String::from("hello");
    let y = x; // Now ‘y’ owns the string
    // dbg!(x, y); // Cannot use ‘x’ since it owns nothing!
}

This is important: When we say let mut x = thing; let mut y = x, are updates to y reflected in x or not? Yes, if thing is a literal; otherwise it doesn't matter in Rust since x can no-longer be used —multiple mutable references cause trouble!

Exercise! Confirm that references cannot be owned, by making a similar x, y setup as above.

(For the curious, String is just a structure consisting of a vector of integers.)

Incidentally, another difference between literals and objects is their lifetimes. (Objects are heap-allocated.)

fn main() {
    // This works since literals are “static”; live forever.
    let x = { let y = &"hi"; y };

    // let x = { let y = &"hi".to_string(); y };
    // Error: The string object does not live long enough to be used anywhere else!
    // Note: Consider using a `let` binding to create a longer lived value.

    dbg!(x);
}

Exercise! By making similar x, y setups as above, confirm that arrays [T; n] are unowned and have static lifetimes, whereas vectors Vec<T> are owned and are only have dedicated lifetimes.

5. Types

Rust is a statically & strongly typed language: It knows the types of all variables at compile time —it can infer them if you don't write them— and it does no implicit type coercions.

Type Explanation ; Example value
Integers i32, u32 Number without decimals ; 123
Floats f32, f64 Numbers with decimals ; -1.23, 1e4
Booleans bool Truth values ; true, false, !p, p && q, p ∥ q
Characters char Unicode characters ; 't', '😻'
Tuples (τ₀, τ₁, …, τₙ₋₁) \(n\) things of different types ; (1, 'a', 2.3)
Unit () A “zero-tuple”; denotes “done”; value is written ()
Arrays [τ; n] \(n\) things of the same type ; [1, 2, 3]
Vectors Vec<τ> Like arrays, but can increase in size ; vec![1, 2, 3]
Slices [τ] Slices are similar to arrays, but their length is not known at compile time.
Structures A record, hashmap, lightweight class; key-value pairs
Enums Constants; algebraic data-types
Ranges \(m..n\) is the sequence of integers \(i\) with \(m ≤ i < n\). ; 3..7
Functions (τ₁, …, τₙ) → τ Transform τᵢ into a τ ; fn first<A, B>(a:A, b:B) -> A { a }
[Read-only] References &τ Refer to an object, without owning it ; let s = ⋯; &s ;; let x = &4
Mutable references &mut τ Borrow an object and change it ; let mut s = ⋯; &mut s ;; let x = &mut 4
String Slices, &str A string literal that can only be borrowed, but not owned/mutated; let five = "5"
[Growable] Strings, String Strings that can grow in size; essentially Vec<&str>;; let five = "5".to_string()
  • Slices are the answer to “What is the input type of a function that consumes arrays but doesn't know their length?” Slices act like temporary views into an array or a vector.
  • Rust vectors are array-lists: They're arrays that double in size whenever more space is needed.
  • Range Example: (1..5).for_each(|i| println!("{}", i));
  • The unit type, (), is often used as the return type of functions that have side-effects, such as printing to the screen. Everything has to have a type, so () (which can be read “done”) can be that type. The default return type of functions is (), unless explicitly indicated otherwise.
  • For 𝓃 in {8, 16, 32, 64}, a number of bits, there are signed integers i𝓃 which can store numbers from \(-2^{n}\) to \(2^{n - 1} - 1\) inclusive, and unsigned integers u𝓃 which store numbers from \(0\) to \(2^{n}\).

    • These types provide bounds on numbers; e.g., let it: u8 = 256 results in a compile error since 256 is out of range for u8.
    • By default, numeric variables without an explicit type are assigned type i32, if possible.
    • Numbers have the expected arithmetic operations +, -, /, % (remainder).
    • Underscores can be inserted in numeric literals to improve readability, e.g. 1_000 is the same as 1000, and 0.000_001 is the same as 0.000001.

6. Tuples and Arrays

Both tuples and arrays collect multiple values and have fixed (non-growable) sizes; the only difference is that the values in a tuple can be of a different type whereas an array requires them to all have the same type.

fn main() {
    // Create: Tuples, different types allowed
    let p = ('a', 1, 2.3, "bye");
    let q: (char, i32) = ('a', 12); // Explicit type annotation

    // Create: Arrays, can only be same type
    let a = [1, 2, 3];
    let b : [char; 2] = ['x', 'y']; // Explicit type annotation

    // Read: Tuples, with “ .𝒾 ” notation
    // Read: Arrays, with “ [𝒾] ” notation
    println!("{0}, {1}, {2}", q.0, p.3, a[1]); // p.4, a[4] ⇒ Error!

    // Both can be *destructured*, but must match in length
    let (_, me, _, you) = p; // ≈  let me = p.1; let you = p.3;
    let [_, them, _]    = a; // ≈  let them = a[1];

    // Example: A super simple way to swap
    let x = 1; let y = 2;
    assert!(x == 1 && y == 2);
    let (y, x) = (x, y);       // Shadowing
    assert!(x == 2 && y == 1);

    // Shorthand for constant arrays
    assert!([2, 2, 2, 2] == [2; 4]);
}

[τ; n] is the type of arrays of length \(n\) with elements from type τ.

If τ is a value, then there is only one possible array: [τ, τ, ..., τ] (n-many times).

We can loop over an array as follows.

fn main() {
    let names = ["me", "you", "them"];
    for name in names {
        println!("{}", name)
    }
}

Exercise! Use a vector, let names = vec!["me", "you", "them"], and try to (debug-)print names after the loop. Notice that name lost ownership of its value to the for⋯in loop. Fix this by giving the loop a reference, &names.

7. Slices —and str

If we have an array/vector/string/collection, how do we reference a slice of their memory; i.e., narrow our view to a sub-part of the entire whole? The answer is “slice types”.

Slices are similar to arrays, but their length is not known at compile time. Essentially, slices are “views into segments of data” and so consists of two-parts: a pointer to the start of the data, and the length of the data. Below, with a bit of pointer arithmetic, we can use slices as these two-parts —but this is not recommended!

  • A slice, [τ], is a view into a block of memory. Whether mutable or not, a slice always borrows and that is why it is always behind a pointer, &. Thus, sometimes people say “slices are &[τ]”.
  • Since references are unowned, so too slices &[τ] are unowned.
fn main() {
    let array: [i32; 5] = [11, 22, 33, 44, 55];
    let slice: &[i32] = &array;

    // Decompose a slice into its two main parts
    let length = slice.len(); // ⇒ 3
    let pointer = slice.as_ptr(); // ⇒ Some memory address

    // The 𝓃th element is 𝓃-steps away from the starting pointer.
    // (This is why elements start at index 0.)
    dbg!(unsafe { *pointer.offset(1) }); // ⇒ 22

    // If you do a pointer offset out of bounds, like 4, you've just accessed more memory;
    // possibly by accident and not the memory you want!

    // Safe way to do indexing; which checks for bounds at run-time.
    let element = slice[2]; // ⇒ 33
                            // let element = slice[4]; // Index out of bounds error!

    // An even safer way, that does not crash at run-time.
    let element = slice.get(4); // ⇒ None

    // Get a sub-slice start at index 2 up to, and including, index 3.
    let sub: &[i32] = &slice[2..=3]; // ⇒ [33, 44]

    // You can (mutably) borrow a slice of a (mutable) vector, or string, etc.
    let mut v = vec![1, 2, 3, 4, 5];
    let s = &mut v[2..]; // ⇒ [3, 4, 5]
    s[0] = 333;
    assert!(s == [333, 4, 5]);
    assert!(v == vec![1, 2, 333, 4, 5]);
}

We borrow slices of x : τ using &x[..] provided the type τ implements the Index and IndexMut traits, which let us using the indexing operator. The “..” syntax is for range literals: .., 0.., ..20, ..=20, 3..6 are all example ranges with m..n denoting all integers \(i\) with \(m ≤ i < n\) —and the right-bound can be made inclusive using ‘=’. More concretely, (..=20).contains(&13) is true since the range ..=20 is all integers at-most 20.

  • References are implicitly converted into slices, when possible, by treating &x as if it were &x[..].

[Mutable Slices] Notice that changing the value of a slice index also changes the value in the underlying array/vector/string —which actually owns the values that the slice is letting us look at.

Since slices can be formed from vectors and arrays, if we want a function to work on both kinds of values, we make our function consume a slice: Values of &Vec<T> and &[T; n] are implicitly transformed into values of &[T].

  • Likewise, for &str and String, the more generic approach is functions consuming &str.

Rust's strings are all Unicode and so characters differ in size, as such string slices cannot be implemented with a pointer and accessed with a uniform offset, like [T]. As such, there is a special type str, “string slices”, which fullfills this role. str really acts like slice types; e.g., "hello" has type str and &"hello"[1..] has type &str; yet assert!(&"hello"[1..] == "ello") is true.

8. Structures —“Tuples with named and unordered components”

Structures provide ways to group several values into a single value. Conceptually, this allows us to put a bunch of related things in a bag and run around with the bag, instead of wrapping our arms around all of the individual things and trying to hold on to them separately. ---Eloquent JavaScript

A structure is also known as a record, hashmap, association list, lightweight-class, JSON object. Struct types are formed as name-type pairs, and their values are constructed with ‘struct literals’: Name-value pairs preceded by the struct name.

  • Tuples quickly group related variables together, but structs give us meaning by naming the variables —instead of using numeric indices.

    // ⟨0⟩ Forming a new type
    #[derive(Debug)]   // Simple debug printing
    struct Person {
        name : String,
        job  : String,
        age  : u32
    }
    
    // ⟨1⟩ Introducing a value of that type
    let mut job = String::from("farmer");
    let mut jay = Person {
        name: String::from("jason"),
        age: 72,
        job // Shorthand for same name, but *cloned* value:   job: String::form("farmer")
    };
    // Mutability of “job” and “jay” are not related at all.
    job = String::from("labourer");       // Has no effect on “jay”
    assert!(jay.job == "farmer");
    
    // ⟨2⟩ Eliminating a value of that type by destructuring
    // [Ownership moves: If “jay.field” is an object, it has moved to “field”]
    // [Namely, after this line, /read access/ of “jay.name” is invalid.]
    let Person {name, age, ..} = jay; // The “..” are to ignore the rest
    
    // ⟨3⟩ Updating a field [Give “jay.name” a value and so it is valid to read.]
    jay.name = String::from("jasim");
    assert!(jay.name == "jasim" && name == "jason");
    
    // Using the automatic debug printing
    println!("Jay is {:?}", jay); // Pretty print with: {:#?}
    
    // ⟨4⟩ Let's attach a method to our struct instances
    impl Person {
        fn speak(&self) {
            println!("{}: I am a {} years old.", self.name, self.age);
        }
    }
    // Let's use this method.
    jay.speak();
    
    // ⟨1⟩' Creating instances from instances; “prototyping”!
    // [Read: kathy is like jay but has a different name.]
    // [This “struct update syntax” can only happen in the last position.]
    // [In-particular, “Person {..jay}” is a clone of the value “jay”.]
    let kathy = Person {name: String::from("Kalthum"), ..jay};
    // ⟨4⟩′ We can call methods using type-namespace syntax.
    Person::speak(&kathy);
    
    // ⟨1⟩″ We can also make unnamed fields, and use tuple indexing
    // These “tuple structs” are basically named tuples.
    struct Mine(String, u32);
    let mut it = Mine(String::from("hola"), 23);
    it.1 = 12;
    

// ⟨4⟩ The function fn greet(p: &Person) { println!("{}: I am a {}.", p.name, p.job) } is very specific: It only works on Person values. As such, it's helpful to tie this behaviour more closely to the Person struct, which can be done by moving it into an “impl”ementation block. All methods in an impl block must have the first argument named self. As such, we could have written fn speak(self: &Person) {⋯}. However, within an impl block, the type Self is an alias for the type that the impl block is for. As such, we could have written fn speak(self: &Self) {⋯}. Since the first argument of methods must be named self and typed Self, we can just abbreviate it to self: Self to self —however, in our case we use the abbreviation &self because we want the value to be borrowed, not owned.

Exercise! Change speak above to consume a mutable reference, &mut self, that increments the age. Two other non-mutable things will now break in the above program; make them mutable. Make kathy speak twice and assert! her age has changed.

Exercise! Struct methods can have the same name as struct fields —whereas arbitrary functions cannot share the name of variables. Change the method name speak to be age.

8.1. Structs can be generic too!

struct Pair<A, B> {
    first: A,
    second: B,
}

fn main() {
    let p = Pair {
        first: 3,
        second: true,
    }; // : Pair<i32, bool>
}

9. Enumerations ~ Tagged Payloads

The enum keyword allows the creation of a type which may be one of a few different variants.

Let's make an generic enum to indicate where something can be found, amongst two kinds of types.

// An enum is defined by a collection of structs.
#[derive(Debug)]
enum Location<A, B> {
    // An `enum` variant may either be unit-like,
    NoWhere,
    // or tuple structs,
    Here(A),
    There(B),
    // or named structures.
    Both { first: A, second: B },
    // Besides being generic, enums can also be recursive (via indirection)
    // Swap(Box<Location<A, B>>),
}

// To work on enums, we use the match clause
fn toInt<A, B>(l: &Location<A, B>) -> i32 {
    match l {
        Location::NoWhere => 0,
        Location::Here(a) => -1,
        Location::There(b) => 1,
        Location::Both { first, second } => 2,
    }
}

fn main() {
    let both = Location::Both {
        first: 3,
        second: false,
    };
    let nope = Location::NoWhere::<i32, i32>;

    // Use names without qualifiers
    use crate::Location::*;
    let no: Location<i32, i32> = NoWhere;

    dbg!(&no, toInt(&no)); // ⇒ &no = NoWhere; toInt(&no) = 0
}

Exercise! Insert a use clause within toInt so that you can use the constructor names without qualifying them.

Author: Musa Al-hassy

Created: 2022-04-18 Mon 08:48

Validate