DEV Community

loading...
Cover image for The Rustlings exercises - part 1

The Rustlings exercises - part 1

Nicolas Frankel
Dev Advocate | Former developer | Former architect | Former teacher | Still learning and blogging.
Originally published at blog.frankel.ch ・5 min read

To continue building my understanding of Rust, I searched for some simple Rust exercises. Hence, I dedicated my weekly personal work time to the Rustling exercises.

Greetings and welcome to rustlings. This project contains small exercises to get you used to reading and writing Rust code. This includes reading and responding to compiler messages!

-- https://github.com/rust-lang/rustlings

I believe that this workshop is pretty neat. Thanks to everybody who contributed to it!

I'll split my notes into two posts, as Rustlings contain many (many!) exercises. Besides that, I need to learn about the more advanced themes such as threading.

In those two posts, I'll only describe the exciting bits. If you're interested, you can find the solutions themselves on GitHub. I'd urge you to try by yourself, though.

Conditionals

The first exercise is about conditionals. To return a bool, the following snippet doesn't compile:

if a > b {
  a
}
b
Enter fullscreen mode Exit fullscreen mode

Instead, you have to be explicit about this:

if a > b {
  a
} else {
  b
}
Enter fullscreen mode Exit fullscreen mode

Move semantics

The real "fun" about data ownership starts here. Exercises in this folder require you to wrap your head around ownership and borrowing.

let vec0 = Vec::new();

let mut vec1 = fill_vec(vec0);          // 1
Enter fullscreen mode Exit fullscreen mode
  1. The vec1 variable now owns the vec0 parameter!

The idea is to pass a reference to vec0 instead so that that fill_vec() only borrows it.

fn fill_vec(vec: &[i32]) -> Vec<i32> { // 1
    let mut vec = vec.to_vec();        // 2
Enter fullscreen mode Exit fullscreen mode
  1. Pass a slice
  2. Create a new Vec from the slice and return it

more_semantics3.rs is much easier as the compiler outputs the correct hint to fix the issue.

Structures

One of the main points in learning a new language is getting familiar with the available ways to design complex models. It's the theme of this series of exercises. In structs1.rs, there are two lessons:

  1. Use &str instead of String.
  2. When using references, we need to take care of the lifetime.

struct2.rs teaches about copying existing structures into new structures. Rust allows you to create a new struct from an existing one by defining only different field values and copying the same ones using ... You can read more about it here.

Collections

Another significant point is learning collections. This series is on Vec and HashMap.

fn vec_loop(mut v: Vec<i32>) -> Vec<i32> {
    for i in v.iter_mut() {
        // TODO: Fill this up so that each element in the Vec `v` is
        // multiplied by 2.
    }

    // At this point, `v` should be equal to [4, 8, 12, 16, 20].
    v
}
Enter fullscreen mode Exit fullscreen mode

The syntax already uses mutability, i.e., mut and iter_mut(), so that is not an issue. But iter_mut(), as well as iter(), return references. Hence, we have to dereference both the left and the right side of the assignment.

Regarding HashMap, it seems there's no available macro like vec![] for Vec.

Error handling

After structures and collections, I think error handling completes the trinity of the foundations of a language. For example, Go put me off with its way.

In errors.rs, I got confused: I tried to create a custom Err type. But one only needs to replace Some with Ok, and use the standard Result type.

Option and Result enums

I solved the error2.rs by using match. I forgot that each match clause must end with a comma.

match qty {
    Ok(i) => Ok(i * cost_per_item + processing_fee),
    Err(e) => Err(e),
}
Enter fullscreen mode Exit fullscreen mode

It's not the way. It's much easier to use map() and the proper closure. There's no comma between the closure's parameter(s) and its body as opposed to' match'.

To me, the compiler hint was no help to solve error_handlingn.rs. I made the wrong assumption initially and tried to use CreationError as the return type. It didn't help that I didn't read the book's section about the Box type.

Generics

As opposed to Go, Rust provides generics.

For generics3.rs, I had to use bounds on generics. The syntax is similar to Java's. The difficulty lies in the fact that you must set the bound on both the trait and its implementation.

Options

This series is pretty straightforward and deals with the Option enum.

For option2.rs, I had to re-read the if let syntax. It's an assignment, so it accepts only a single = sign.

Results

result1.rs made me realize that match only matches on values, not on expressions. To check for expressions, use if else.

Tests

Exercises on tests are great. It's a good occasion to check which base assertions are available:

  1. assert()
  2. assert_eq()
  3. assert_ne()

Iterators

The final series for today's post of exercises is on iterators.

In iterator2.rs, I learned that a char.to_uppercase() doesn't return a String but a dedicated ToUpperCase type. The reason is that some languages don't have a simple mapping from lower case to upper case, e.g., German ß.

TIL: join("") function when you need to collect additional items in between. The associated type is std::slice::Join. I didn't find the association between Vec<> and join()...

I started iterator3.rs with list_of_results() because it's (much) easier. division_results is of type Map<IntoIter<i32>, fn(i32) -> ?>. x must be of type <Vec<Result<i32, DivisionError>>>. It means we only need to collect() the Iterator: it will trigger the map() closure. Done.

result_with_list() requires a <Result<Vec<i32>, DivisionError>>. I had to go through the documentation to find the collect() function that applies explicitly to an iterator of Result. The idea is to collect first like in the previous function, then make it back to an iterator of Result, and finally `collect() again.

As mentioned in the documentation, you need to help the compiler with collect() because, in general, it cannot infer types correctly:

Because collect() is so general, it can cause problems with type inference. As such, collect() is one of the few times you'll see the syntax affectionately known as the 'turbofish': ::<>. This helps the inference algorithm understand specifically which collection you're trying to collect into.

-- pub fn collect(self) -> B

Solving iterators4.rs is easy with recursion. The compiler hints about ranges: it's actually relatively easy combining them and the fold() function. The latter is fairly common in Functional Programming.

This is it. In the next post, I'll provide the notes I took while solving the remaining exercises.

The complete source code for this post can be found on Github on the work branch:

GitHub logo ajavageek / rustlings

🦀 Small exercises to get you used to reading and writing Rust code!

All Contributors

rustlings 🦀❤️

Greetings and welcome to rustlings. This project contains small exercises to get you used to reading and writing Rust code. This includes reading and responding to compiler messages!

...looking for the old, web-based version of Rustlings? Try here

Alternatively, for a first-time Rust learner, there are several other resources:

  • The Book - The most comprehensive resource for learning Rust, but a bit theoretical sometimes. You will be using this along with Rustlings!
  • Rust By Example - Learn Rust by solving little exercises! It's almost like rustlings, but online

Getting Started

Note: If you're on MacOS, make sure you've installed Xcode and its developer tools by typing xcode-select --install.

You will need to have Rust installed. You can get it by visiting https://rustup.rs. This'll also install Cargo, Rust's package/project manager.

MacOS/Linux

Just run:

curl -L https://git.io/rustlings | bash
# Or if you want it
Enter fullscreen mode Exit fullscreen mode

To go further:

Originally published at A Java Geek on June 13th 2021

Discussion (0)