ForgeStream

The thing I want to share with you as a beginner who just started learning rust


Learning

As a beginner who has just started formally learning Rust, I’d like to share some of my ideas, questions, and the approach I used to learn in this topic.

To Start

Rustlings + Rust By Example

Rustlings is an excellent way to start learning Rust. It’s easy to set up by simply running cargo install rustlings. Once installed, you can start using the rustlings command.

There are a total of 23 exercises that cover most of the topics in Rust by example documentation, along with 3 quizzes to demonstrate how each topic can be applied in a project.

It’s recommended to check the README.md for related reference articles on the topic before starting the exercises. The questions cover some, but not all, of the content. If you only focus on completing the questions, you’ll miss out on a lot!

Exercism

Now that you’ve completed Rustlings, Exercism is a great place to continue. Unlike Rustlings, which breaks tasks into individual Rust-by-example topics, Exercism gives you a broader task and requires you to figure out which concepts to apply. There’s no single correct solution on Exercism, so you can approach problems in your own way, which may differ from others.

Interesting topic

In this section, I’ll share some topics that I found confusing during my learning process. These are likely common questions for Rust beginners as well.

String and &str

Unlike PHP or Java, which have only one string type, Rust has two: string slices and String. So, what’s the purpose of having two different data types to store string data?

According to the definition in the Rust-by-Example documentation:

A String is stored as a vector of bytes (Vec), but guaranteed to always be a valid UTF-8 sequence. String is heap allocated, growable and not null terminated.

&str is a slice (&[u8]) that always points to a valid UTF-8 sequence, and can be used to view into a String, just like &[T] is a view into Vec<T>.

Now we know that String is a heap-allocated data type, and its size can be unknown at compile time, while &str is a borrowed reference to a part of a String or a string literal (typically stored in read-only data).

But why not just use String everywhere and get rid of the less powerful &str?

After going through various threads, it seems that it’s recommended that:

“Always use String in structs, and for functions, use &str for parameters and String types for return values.”

Use String for functions

Say we want to have a function to return a String

fn return_some_string(str_1: String) -> String {
    str_1 + &String::from("yes")
}

We cannot use string slice, though, even with a lifetime, because we’re returning a borrowed part of String String::from("oiiaioiiiiai") which only lives till the end of the function:

fn return_some_string_slice<'a>(some_other_string: &'a str) -> &'a str {
    (String::from("oiiaioiiiiai") + some_other_string).as_str()
}

error[E0515]: cannot return value referencing temporary value
  --> src/main.rs:15:5
   |
15 |     (String::from("oiiaioiiiiai") + some_other_string).as_str()
   |     --------------------------------------------------^^^^^^^^^
   |     |
   |     returns a value referencing data owned by the current function
   |     temporary value created here

Use &str for parameters

Using same example above, now we having a main function to call the return_some_string function:

fn main() {
    let str = String::from("oiiaioiiiai);

    println!("{}", return_some_string(str));
    println!("{}", return_some_string(str));
}

We’ll be getting this error:

error[E0382]: use of moved value: `str_1`
  --> src/main.rs:9:41
   |
7  |     let str_1 = String::from("oiiaioiiiiai");
   |         ----- move occurs because `str_1` has type `String`, which does not implement the `Copy` trait
8  |     println!("{:?}", return_some_string(str_1));
   |                                         ----- value moved here
9  |     println!("{:?}", return_some_string(str_1));
   |                                         ^^^^^ value used here after move

Instead of cloning, we could simply, follow the rule, use &str in parameter

fn main() {
    let str_1 = String::from("oiiaioiiiiai");
    println!("{:?}", return_some_string(&str_1));
    println!("{:?}", return_some_string(&str_1));
}

fn return_some_string(some_other_string: &str) -> String {
    some_other_string.to_owned() + "yes"
}

The rule generally works, but there are certainly special cases. For more details, check out this article. When should I use String vs &str?.

Coercions

Another interesting feature of Rust is Coercions. Rust can implicitly convert one type to another, either through built-in mechanisms or by user-defined traits, such as Deref.

Consider the example below, which returns the first two items of the given list:

fn main() {
    let list1 = [1,2,3];

    println!("{:?}", return_other_slice(&list1));
}

fn return_other_slice(list: &[i32]) -> &[i32] {
    &list[0..=1]
}

Note that the parameter list is passed by reference. You might think, then, that if we wanted to return &[i32], we would need to dereference list in the function by returning &(*list)[0..1]. However, if you run the code, you’ll find that the above approach works perfectly.

[1, 2]

The reason for this is that when indexing a list, Rust automatically dereferences it through coercion.

Conclusion

In conclusion, Rust is a fascinating and powerful language. While it may be more challenging than other common languages, it is definitely worth learning.