Rust 101-2

My Rust learning note

November 14, 2023

Preface

šŸ“” I used the

by Google as learning resource. It is used for internal educating at first but recently released to general public. If you want to learn Rust, check out this great resource!

ā—This is the 2nd learning note of this series. If you want to learn from start and itā€™s your first time here, make sure to check out the

.

Alright, letā€™s get started.

Control Flow

Blocks

Definition: A block in Rust contains a bunch of expressions. They are wrapped by {}. The block actually has a value and type, which are defined by the last expression of the block.

Note that, sometimes, you cannot write return in the block. See the wrong example $1.2$ below, it will throw compile error. I guess the compiler will seem the return inside the block as the return for the main function. And the error message will tell you the expression under the first return is unreachable.

But if there are no such conflict, you can feel free to use both. (example $2.1$ & $2.2$)

// āœ… example 1.1 
fn main() {
	// there's a block here and the type would be "int"
	let x = {
		let y = 10
		y = y + 2;
		y // the last expression of the block is the value of the block
	}
	return x;
}
// āŒ example 1.2
fn main() {
	// there's a block here and the type would be "int"
	let x = {
		let y = 10
		y = y + 2;
		return y;
	}
	return x;
}
// āœ… example 2.1
fn double(x: int32) -> int32 {
	x + x
}
// āœ… example 2.2
fn double(x: int32) -> int32 {
	return x + x;
}

If expressions

It basically works like other language.

fn main() {
    let mut x = 10;
    if x % 2 == 0 {
        x = x / 2;
    } else {
        x = 3 * x + 1;
    }
}

But there is a cool expression like this

fn main() {
    let mut x = 10;
    x = if x % 2 == 0 {
        x / 2
    } else {
        3 * x + 1
    };
}

I think itā€™s like ternary operator in Javascript and similar thing in Python

// Javascript
let x = 10;
const x = x % 2 == 0 ? x / 2 : 3 * x + 1 
# Python
x = 10
x = x / 2 if x % 2 == 0 else 3 * x + 1

ā—Note that, in Rust, two branch blocks of the if expression must have the same type.

For loop & While loop

They are both similar to Python.

  • Pythonā€™s range(start, end) can be written in Rust like this (start..end)
  • We can specify step size like this (start..end).step_by(step_size)
  • The usage of break and continue are the same as other languages. But there is a cool thing like this. You can label break and continue, and while loop to choose which loop you want to break or continue when there are nested while loops.
    fn main() {
        let v = vec![10, 20, 30];
        let mut iter = v.into_iter();
        'outer: while let Some(x) = iter.next() {
            println!("x: {x}");
            let mut i = 0;
            while i < x {
                println!("x: {x}, i: {i}");
                i += 1;
                if i == 3 {
                    break 'outer;
                }
            }
        }
    }	
    In this case, we break the outer loop after 3 iterations of the inner loop. The output world be
    x: 10
    x: 10, i: 0
    x: 10, i: 1
    x: 10, i: 2

loop expression

Itā€™s an infinite loop. You can think it as while true. The only way to stop is use break or return.

Variables

const

const defines a value which cannot be changed after declaration. To comply convention, we use ALL_UPPER_CASE_AND_UNDERSCORE to name the variable.

const DIGEST_SIZE: usize = 3;
const ZERO: Option<u8> = Some(42);

fn compute_digest(text: &str) -> [u8; DIGEST_SIZE] {
	let mut digest = [ZERO.unwrap_or(0); DIGEST_SIZE];
	for (idx, &b) in text.as_bytes().iter().enumerate() {
		digest[idx % DIGEST_SIZE] = digest[idx % DIGEST_SIZE].wrapping_add(b);
	}
	digest
}

fn main() {
	let digest = compute_digest("Hello");
	println!("digest: {digest:?}");
}

In above code snippet:

  • Type Option<T> is a Rust enum that represents an optional value: either a value of type T(need to wrap the value in Some) exists or does not exist(None). Itā€™s similar to T? in Typescript.
    fn divide(numerator: f64, denominator: f64) -> Option<f64> {
        if denominator == 0.0 {
            None
        } else {
            Some(numerator / denominator)
        }
    }
  • unwrap_or(value) is a method to unwrap Option<T>(produced by Some()), if the value is not None, unwrap_or will return its value, else if itā€™s None, it will return value.
  • string.as_bytes() converts string into a byte slice. And then iter() creates an iterator allows us to loop over it. enumerate() is identical to Pythonā€™s enumerate, which let you to access index of the slice.
  • wrapping_add is to prevent overflow at runtime, which can cause panic. After adding this, it will provide function like modulo operation. So, letā€™s say we have an u8 equal to its max value 255. After adding 1 by wrappin_add(1), it will become 0 rather than overflow. `

static

Static variables will live during the whole execution of the program, and therefore will not move. Itā€™s similar to const but different in some ways.

  • const is compile time constant; static is runtime constant.
  • It means const will be copy to each line that use that const at compile time. Therefore, const donā€™t have a fixed memory location.
  • static is still there at runtime. It has fixed memory location.

Shadowing

fn main() {
    let a = 10;
    println!("before: {a}");

    {
        let a = "hello";
        println!("inner scope: {a}");

        let a = true;
        println!("shadowed in inner scope: {a}");
    }

    println!("after: {a}");
}

/* 
before: 10  
inner scope: hello  
shadowed in inner scope: true  
after: 10
*/

Itā€™s different from mutation because when shadowing, both variableā€™s (with same name) memory locations exist at the same time.

Enums

An enum itself is a type(like WebEvent below). An enum can have several Variants. Variant can have payload (like KeyPress and Click below). Note that we need to write namespace to access the Variant, like WebEvent::PageLoad.

enum WebEvent {
    PageLoad,                 // Variant without payload
    KeyPress(char),           // Tuple struct variant
    Click { x: i64, y: i64 }, // Full struct variant
}

We can use match (like switch and case in other languages) to do pattern matching. Note that there is no fall-through like other languages, so we donā€™t need to add break for each case.

fn inspect(event: WebEvent) {
    match event {
        WebEvent::PageLoad       => println!("page loaded"),
        WebEvent::KeyPress(c)    => println!("pressed '{c}'"),
        WebEvent::Click { x, y } => println!("clicked at x={x}, y={y}"),
    }
}

Novel Control Flow

if let

It let you execute different blocks depending on whether a value matches a pattern.

fn main() {
    let arg = std::env::args().next();
    if let Some(value) = arg {
        println!("Program name: {value}");
    } else {
        println!("Missing name?");
    }
}

In the above code, Some(value) can either be value or None depending on the arg.

while let

Similar to if let, if the while let failed, the loop will be break. In the example, iter has type Option<i32>.

fn main() {
    let v = vec![10, 20, 30];
    let mut iter = v.into_iter();

    while let Some(x) = iter.next() {
        println!("x: {x}");
    }
}

// Actually, while let is a syntax sugar, we can produce this kind of effect by using if let
fn main() {
	let v = vec![10, 20, 30];
    let mut iter = v.into_iter();

	loop {
		if let Some(x) = iter.next() {
			// do something
		}
		else {
			break;
		}
	}
}

Pattern Matching

Destructing Structs

Pattern matching match expression can not only destruct Enums(e.g. the WebEvent example above), it can also destruct structs.

struct Foo {
    x: (u32, u32),
    y: u32,
}

#[rustfmt::skip]
fn main() {
    let foo = Foo { x: (1, 2), y: 2 };
    match foo {
        Foo { x: (1, b), y } => println!("x.0 = 1, b = {b}, y = {y}"),
        Foo { y: 2, x: i }   => println!("y = 2, x = {i:?}"),
        Foo { y, .. }        => println!("y = {y}, other fields were ignored"),
    }
}

// x.0 = 1, b = 2, y = 2

In this example, the first condition matches struct Foo when x.0 == 1. The second one catches when y==2. And the last expression is the wildcard, and it only need to use yā€™s value. Note that, in this example, even y==2, since the first line matches first. It will print x.0 = 1, b = 2, y = 2 instead of y = 2, x = (1, 2). So, order matters.

Destructing Arrays

Of course, you can also destruct arrays or tuple. The code looks very similar as above example and self-explainable.

#[rustfmt::skip]
fn main() {
    let triple = [0, -2, 3];
    println!("Tell me about {triple:?}");
    match triple {
        [0, y, z] => println!("First is 0, y = {y}, and z = {z}"),
        [1, ..]   => println!("First is 1 and the rest were ignored"),
        _         => println!("All elements were ignored"),
    }
}

Match Guards

When pattern matching, you can also write some expression to achieve some custom logics. This is called Match Guards. Itā€™s a flexible way to do pattern matching. The arm will be triggered only when the boolean expression return true.

#[rustfmt::skip]
fn main() {
    let pair = (2, 2);
    println!("Tell me about {pair:?}");
    match pair {
        (x, y) if x == y     => println!("These are twins {x}"),
        (x, y) if x + y == 0 => println!("Antimatter, kaboom!"),
        (x, _) if x % 2 == 1 => println!("The first one is odd"),
        _                    => println!("No correlation..."),
    }
}

Exercise

Luhn algorithm

:

TheĀ 

Ā is used to validate credit card numbers. The algorithm takes a string as input and does the following to validate the credit card number:

  • Ignore all spaces. Reject number with less than two digits.
  • Moving fromĀ right to left, double every second digit: for the numberĀ 1234, we doubleĀ 3Ā andĀ 1. For the numberĀ 98765, we doubleĀ 6Ā andĀ 8.
  • After doubling a digit, sum the digits if the result is greater than 9. So doublingĀ 7Ā becomesĀ 14Ā which becomesĀ 1 + 4 = 5.
  • Sum all the undoubled and doubled digits.
  • The credit card number is valid if the sum ends withĀ 0.

My solution:

  • First, I loop through the string and see if thereā€™s any non-digit character. If found, return False directly. Note that |c| c.is_alphabetic() is how Rust write Lambda function like Python.
  • Then, I filter out the spaces in the string and convert the digit character to u8 and collect by Vec.
  • Next, I just perform the ā€œDoublingā€ step as problem setting mentions above.
  • Sum and return bool.
fn doubling(x: u32) -> u32 {
    let mut tmp = x;
    tmp *= 2;
    if tmp > 9 {
        tmp = 1 + (tmp % 10);
    }
    tmp
}

pub fn luhn(cc_number: &str) -> bool {
    // Check for English letters
    if cc_number.chars().any(|c| c.is_alphabetic()) {
        return false;
    }
    // filter and convert str to vec<u8>
    let mut vec: Vec<u32> = cc_number.chars().filter(
        |c| c.is_digit(10)
    ).filter_map(
        |c| c.to_digit(10)
    ).collect();
    // reject if less than 2 digits or empty
    if vec.len() < 2 {
        return false
    }
    // traverse and apply doubling operation
    let is_odd: bool = vec.len() % 2 == 1;
    for i in 0..vec.len() {
        let digit = vec[i];
        if is_odd {
            if i % 2 == 1 {
                vec[i] = doubling(digit);
            }
        } else {
            if i % 2 == 0 {
                vec[i] = doubling(digit);
            }
        }
    }
    // sum all up
    let summation = vec.iter().fold(0u32, |sum, i| sum + (*i as u32));
    return summation % 10 == 0;
}
avatar

About author...

Shu-Wei (Frank) Hsu
šŸ“ NY-based Full stack Dev šŸ—½
Passionate about Web Development and UI/UX.
linkedin github threads email