My Rust Journey - 5th November 2024

This is the 7th post about my journey learning the Rust programming language using the Rust Book. Previous posts include:

Chapter 1: Basics of Rust and Cargo

Chapter 3: Mutability and shadowing, variables and constants, scalar and compound data types, functions, control flow with conditional statements and loops

Chapter 4: Ownership, reference and borrowing, and slice type

Chapter 5: Ownership, reference and borrowing, and slice type

Chapter 6: Enums, Control Flow and Matching

Chapter 7: Packages, Crates and Modules

I am documenting this as I think it is a useful thing to do for people interested in learning Rust from my non-developer perspective.

At this stage, you have already installed Rust on your machine, and you are ready to write and run your first Rust programs.

I am using VS code with the rust-analyzer extension. I am working on an M1-mac.

The following tutorial will cover Chapter 8 of the Rust Book. It is meant to be a summary and used with the book as a complementary source of information.

Vectors

See below ways to create a vector.

    let v: Vec<i32> = Vec::new(); //creating empty vector

    let mut v = Vec::new();

    v.push(5); //pushing values into a vector
    v.push(6);
    v.push(7);
    v.push(8);

    let v = vec![1, 2, 3, 4, 5]; //create a vector with vec! macro

We can create a vector using Vec<T> or the macro vec!. The first time we specify v we need to specify the type as well because we are creating an empty vector and Rust cannot know what is in it at compile time. If we create v and then push numbers into it, Rust can infer and we do not to specify the type when we create the empty vector. At compile time v will contain some values.

We can extract the third value in v by taking a reference to it: &v[2].

We can also do some additional operations. If we use the get method we end up with an Option<T> that we can use with match.

let third: Option<&i32> = v.get(2);
    match third {
        Some(third) => println!("The third element is {third}."),
        None => println!("There is no element."),
    }

This will allow our program not to enter panic if we specify and index that is outside our vector.

We can also iterate through a vector using for loops.

let mut v = vec![100, 32, 57];
    for i in &mut v {
        *i += 50;
        println!("{i}");
    }

In this case, we make the vector mutable so that when we iterate through it we can do some operations to modify its elements. Not how to change the value that the mutable reference refers to we need to dereference with the * operator.

We can define an enum to store values of different types into one vector.

enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

let row = vec![ //within the main function
        SpreadsheetCell::Int(3),
        SpreadsheetCell::Text(String::from("blue")),
        SpreadsheetCell::Float(10.12),
    ];

The enum specifies the types we can have in a spreadsheet cell. The vector constructs the row with different types.

Storing UTF-8 Encoded text with Strings

We can specify a string literal and then specify the String type using the to_string method or the usual String::from().

let data = "foo";
let s = data.to_string();

let mut s = String::from("foo");

We can update a string using the push_str method or the push method for char literals.

s.push_str("bar");

println!("{s}");
    
s.push('!');

println!("{s}");

We can also combine different strings:

let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; //s1 moved to s3

The + operator uses the add method that takes a reference to a string &str as input. This is why we used &s2. But &s2 is &String type. The add method uses a deref coercion turning &s2 into &s2[..]. Because the method does not take ownership of the input parameter, s2 is still valid after the operation.

For concatenating multiple strings we can use the format macro.

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = format!("{s1}-{s2}-{s3}");

Iterating over strings

You can iterate over strings using the bytes or chars methods.

for b in "hello".chars() {
        println!("{b}");
    }

For more information about indexing strings see the Rust book.

Hash maps

Hash maps are the last type of collection for this chapter (after vectors and strings). Like vectors, hash maps store data on the heap memory and are homogeneous (key and values must have the same type).

Hash maps take the form HashMap<K, V> where K are the keys and V the values.

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);

The score has the value associated with the β€œBlue” key. The get method returns an Option<&i32>, but copied actually returns Option<>i32. The unwrap_or sets the score to 0 if no key exists. We can iterate through the Hash map as follows:

for (key, value) in &scores {
        println!("{key}: {value}");
    }

Once values are inserted into a hash map, they are owned by it. Values with Copy trait are copied into it, but owned values are moved into it.

Each unique key can have one associated value at a time.

We can modify hashmaps as follows:

scores.insert(String::from("Blue"), 25); // overwriting

scores.entry(String::from("Yellow")).or_insert(200); //adding a key and value if not present
scores.entry(String::from("Blue")).or_insert(100);

The entry method takes the key we want to check as a parameter and gives back an enum called Entry that represents a value that might or might not exist. The or_insert method on Entry returns a mutable reference to the value of the corresponding key if it exists, or it attaches a new key with the new value. In this case, both keys already exist, and the code won’t do anything.

The code below counts the occurrence of words in a sentence.

let text = "hello world wonderful world";

let mut map = HashMap::new();

for word in text.split_whitespace() {
    let count = map.entry(word).or_insert(0);
    *count += 1;
}

println!("{:?}", map);

The split_whitespace method returns an iterator over subslices separated by space. At each iteration, the loop adds word and inserts 0 if there is no value, adding +1. If a word occurs more than once, it just adds +1.

In this chapter, we learned about common collections in Rust. See you in the next post!

0
filippoweb3Post author

πŸ”₯ Web3 explained from the non-developer's POV. πŸš€ Helping Polkadot users explore the ecosystem with confidence. I post daily on socials. Opinions are mine.

0 comments

πŸ”₯ Web3 explained from the non-developer's POV. πŸš€ Helping Polkadot users explore the ecosystem with confidence. I post... Show More