rukยทsi

๐Ÿฆ€ Rust
Basics

Updated at 2024-01-04 06:26

Compiler-driven development at its best.

Install

Rust has three important parts in a standard setup:

  • rustup, a tool for Rust version management
  • rustc, the compiler
  • cargo, the package manager

I personally use mise to manage my programming environments, but you might want to just use rustup to install Rust.

rustup show
rustup install stable
rustup default stable
rustup update
// hello.rs
fn main() {
    println!("Hello World!");
}
rustc hello.rs; ./hello
# Hello World!

Cargo

Cargo is the Rust package manager; similar to pip in Python or npm in Node. You use it to manage your Rust projects and their dependencies.

mkdir my-project
cd my-project
cargo init --bin
# you would use --lib if you were creating a library
# cargo init --lib

tree -a .
# .
# โ”œโ”€โ”€ Cargo.toml
# โ”œโ”€โ”€ .git
# โ”œโ”€โ”€ .gitignore
# โ””โ”€โ”€ src
#     โ””โ”€โ”€ main.rs

cat Cargo.toml
# [package]
# name = "my-project"
# version = "0.1.0"
# authors = ["ruksi <me@ruk.si>"]
# edition = "2021"
#
# [dependencies]

cat src/main.rs
# fn main() {
#     println!("Hello, world!");
# }

cargo run
#    Compiling my-project v0.1.0 (/home/ruksi/my-project)
#     Finished dev [unoptimized + debuginfo] target(s) in 0.19s
#      Running `target/debug/my-project`
# Hello, world!

# you would run tests while developing...
cargo test

# and the finally compile a releasable binary
cargo build --release
./target/release/my-project
# Hello, world!

It is idiomatic Rust to include unit tests next to the implementation.

fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*; // import everything from outer scope, even private stuff

    #[test]
    fn test_add() {
        assert_eq!(add(1, 2), 3);
    }
}

Namespaces

The double-colon :: traverses namespaces.

fn main() {
    // `std` is a crate namespaces
    // `cmp` is a module namespace
    // `min` is a function in that module
    let least = std::cmp::min(2, 8);
    assert_eq!(least, 2);
}

use introduces names to the current namespace.

use std::cmp::min;
// or e.g. `use std::cmp::{min, max};`
// or e.g. `use std::cmp::*;`

fn main() {
    let least = min(6, 1);
    assert_eq!(least, 0);
}

Rust implicitly inserts use std::prelude::v0::*; on each module. This introduces some common types like Vec, String, Option and Result.

Variables

You define variables with let. The compiler must always know the type of each variable, but you don't always need to specify it as the compiler can infer it from the context. You have the usual primitive types like i8, f64, u32 and the rest. If you need to redefine the variable later, you must use let mut (mutable). Also, variable definitions can be the result of a block like shown below.

fn main() {
    let number1 = 8;
    assert_eq!(number1, 8);

    let mut number2 = 8;
    number2 = 10;
    assert_eq!(number2, 10);

    let number3 = {
        let number4 = 1;
        number4 + 2
    };
    assert_eq!(number3, 3);
}

Rust has two main types of strings: String and &str. &str (a reference to a static array of letters) and String (a dynamic array of letters) work differently. String is already a pointer to the actual text. String is less performant with but more flexible.

fn main() {
    let name1: &str = "Ruksi";
    let name2: String = String::from("Ruksi");
    assert_eq!(name1, name2);  // many functions play well with both though
}

Prefixing &str with b makes it a byte string.

fn main() {
    let greeting = b"Hello!";
    println!("{:?}", greeting);  // => [72, 101, 108, 108, 111, 33]
}

Casting is asymmetrical. x as u8 as char essentially means "pretend that his unsigned 8-bit integer x is a character". In Rust char is a number-to-letter mapping and 0โ€“255 contain the most used letters, but char does go over full Unicode content so much larger than max u8; so it is safe to do u8 as char but unsafe to do char as u8.

fn main() {
    let a_char: char = 'A';
    println!("{}", a_char as u8);            // => 65
    let a_number = 65;
    println!("{}", a_number as u8 as char);  // => A; but could be unsafe
}

Tuples work like you'd expect them to.

fn main() {
    let pair = ('a', 17);
    assert_eq!(pair.0, 'a');
    assert_eq!(pair.1, 17);
    let (some_char, some_num) = pair;
    assert_eq!(some_char, 'a');
    assert_eq!(some_num, 17);
}

Shadowing variables is encouraged to keep the code readable.

fn times_two(number: i32) -> i32 {
    number * 2
}

fn main() {
    let final_number = {
        let y = 10;
        let x = 9;            // x starts at 9
        let x = times_two(x); // shadow with new x: 18
        let x = x + y;        // shadow with new x: 28
        x                     // return x: final_number is now the value of x
    };
    println!("The number is now: {}", final_number)
}

There are constants and static variables.

  • constants are allowed to duplicate their data when used
  • static variable data must have the same fixed address in memory
  • static variables can be mutable; although that is an unsafe operation

Ranges

.. means range. Used in slicing.

fn main() {
    let v = vec![1, 2, 3, 4, 5];
    let v2 = &v[2..4];
    assert_eq!(v2, [3, 4]);
}
fn main() {
    assert_eq!((0..).contains(&100), true); // 0 or greater
    assert_eq!((..20).contains(&20), false); // strictly less than 20
    assert_eq!((..=20).contains(&20), true); // 20 or less than 20
    assert_eq!((3..6).contains(&4), true); // only 3, 4, 5
}

Type Aliases

Type aliases define a new name for an existing type. They are used to reduce repetition, and keep the code more readable and maintainable.

type Kilometers = i32;
type Result<T> = Result <T, std::io::Error>;
type MyBox = Box<Fn() + Send + 'static>;

Debugging

dbg! macro is the best way of print debugging. But println! and assert_eq! macros are fine too.

fn main() {
    let name = "Ruksi";
    println!("Name: {}", name);    // Name: Ruksi
    println!("Name: {:?}", name);  // Name: "Ruksi"
    println!("Name: {name:?}");    // Name: "Ruksi"

    // dbg! is nicer as it includes the variable name too
    dbg!(name)                     // name = "Ruksi"
}

format!() is the same as print!() but returns a String instead of printing it.

fn main() {
    let toy = "Lego";
    let text = format!("Toy Name: {}", toy);
    assert_eq!(text, "Toy Name: Lego");
}

{} calls std::fmt::Display and {:?} calls std::fmt::Debug. Here are the rest of the most common helpers:

{}    = display format, the
same as String::from()
{:?}  = debugging format
{:#?} = pretty debugging print
{:p}  = the pointer memory address
{:b}  = binary
{:x}  = hexadecimal
{:o}  = octal

You can give indices and names to format! placeholders. Can help future-proof String constructions a bit.

fn main() {
    let one = "1";
    let two = "2";
    let three = "3";
    let text = format!("Numbers are: {what}, {1}, {0}", one, two, what=three);
    assert_eq!(text, "Numbers are: 3, 2, 1");
}

panic! macro violently stops the program with an error message. Very violent, but sometimes useful for debugging.

fn main() {
    panic!("This panics");
}

Structs

Structs are like classes; types you attach methods into.

struct Vec2 {
    x: f64,
    y: f64,
}

impl Vec2 {
    fn is_positive(self) -> bool {
        self.x > 0.0 && self.y > 0.0
    }
}

fn main() {
    let v1 = Vec2 { x: 1.0, y: 3.0 };
    assert_eq!(v1.x, 1.0);
    assert_eq!(v1.y, 3.0);
    let v2 = Vec2 { x: -2.0, y: 1.5 };
    assert_eq!(v1.is_positive(), true);
    assert_eq!(v2.is_positive(), false);
}

Sources