Skip to main content

Rustlings for Beginners

Here are my solutions for Rustlings exercises.

12.3 Option Type and Borrowing​

Problem​

#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}

fn main() {
let optional_point = Some(Point { x: 100, y: 200 });

// TODO: Fix the compiler error by adding something to this match statement.
match optional_point {
Some(p) => println!("Co-ordinates are {},{}", p.x, p.y),
_ => panic!("No match!"),
}

println!("{optional_point:?}"); // Don't change this line.
}

There are two possible solutions for this:

  1. Change optional_point on line 11 to &optional_point

    match &optional_point {
    Some(p) => println!("Co-ordinates are {},{}", p.x, p.y),
    _ => panic!("No match!"),
    }
  2. Change Some(p) on line 12 to Some(ref p)

    match optional_point {
    Some(ref p) => println!("Co-ordinates are {},{}", p.x, p.y),
    _ => panic!("No match!"),
    }

In this case, what is the difference between & and ref?

Explanation​

The Difference Between & and ref (What is ref?)​

&x is an operator that represents a reference to x, while ref x is an operator that instructs the compiler not to move x. As a result, ref x creates a reference to x. In a match expression, Rust attempts to move the received value, which means it also tries to move the contents inside Some. Below is an example from Stackoverflow:

let robot_name = &Some(String::from("Bors"));

match robot_name {
Some(name) => println!("Found a name: {}", name),
None => (),
}

println!("robot_name is: {:?}", robot_name);

In this case, since robot_name is &Some, Some(name) won't match — you need &Some(name). Furthermore, the compiler tries to move the contents name inside Some, but since Some is a reference, it cannot be moved. So you use ref to write Some(ref name), instructing the compiler not to move it.

let robot_name = &Some(String::from("Bors"));

match robot_name {
&Some(ref name) => println!("Found a name: {}", name),
None => (),
}

println!("robot_name is: {:?}", robot_name);

This way, robot_name remains &Some, and name becomes &str without being moved.

13.4 Ordering (Enum)​

This is an error handling exercise, but it's a good study of how to use Ordering.

Problem​

Fix the new function below so that it returns the correct CreationError variant depending on the input value.

#[derive(PartialEq, Debug)]
enum CreationError {
Negative,
Zero,
}

#[derive(PartialEq, Debug)]
struct PositiveNonzeroInteger(u64);

impl PositiveNonzeroInteger {
fn new(value: i64) -> Result<Self, CreationError> {
// TODO: This function shouldn't always return an `Ok`.
Ok(Self(value as u64))
}
}

Explanation​

Without Using the Ordering Enum​

An example of combining if-comparisons with pattern matching. In this case, there's no particular need to use match.

impl PositiveNonzeroInteger {
fn new(value: i64) -> Result<Self, CreationError> {
// TODO: This function shouldn't always return an `Ok`.
match value {
x if x < 0 => Err(CreationError::Negative),
0 => Err(CreationError::Zero),
x => Ok(PositiveNonzeroInteger(x as u64)),
}
}
}

Using the Ordering Enum​

Numeric types have a cmp method, which makes the code cleaner.

In Rust terms, cmp is a method available on types that implement the Ord trait. Implementing the Ord trait indicates that the struct is comparable.

impl PositiveNonzeroInteger {
fn new(value: i64) -> Result<Self, CreationError> {
// TODO: This function shouldn't always return an `Ok`.
match value.cmp(&0) {
Ordering::Less => Err(CreationError::Negative),
Ordering::Equal => Err(CreationError::Zero),
Ordering::Greater => Ok(PositiveNonzeroInteger(value as u64)),
}
}
}

In this case, value.cmp(&0) returns an Ordering enum, and processing branches based on its variants (Less, Equal, Greater).

17.3 should_panic​

Tips for testing cases that should panic.

Problem​

The new method below causes a panic. Write test code to verify this.

// TODO: This test should check if the program panics when we try to create
// a rectangle with negative width.
#[test]
fn negative_width() {
let _rect = Rectangle::new(-10, 10);
}

Explanation​

How to Use should_panic​

should_panic is a macro for confirming that a test panics. It is used as follows:

#[test]
#[should_panic]
fn negative_width() {
let _rect = Rectangle::new(-10, 10);
}

This alone is enough to write a test that confirms Rectangle::new(-10, 10) panics, but the test will pass regardless of the cause of the panic. To pinpoint the cause, you can use expected for a more precise test.

#[test]
#[should_panic(expected = "Rectangle width and height must be positive")]
fn negative_width() {
let _rect = Rectangle::new(-10, 10);
}

18.3 Iterators.collect()​

The collect method can convert an iterator into a collection (such as a vector), and it can also convert an iterator of Result types into a collection.

Problem​

Complete the result_with_list function below. division_results is an iterator that stores the results of dividing each element of numbers by 27 using the divide function. Since divide returns a Result type, division_results is an iterator of Result types.

fn divide(a: i64, b: i64) -> Result<i64, DivisionError> {
if b == 0 {
return Err(DivisionError::DivideByZero);
}
if a == i64::MIN && b == -1 {
return Err(DivisionError::IntegerOverflow);
}
if a % b != 0 {
return Err(DivisionError::NotDivisible);
}
Ok(a / b)
}

// TODO: Add the correct return type and complete the function body.
// Desired output: `Ok([1, 11, 1426, 3])`
fn result_with_list() -> Result<Vec<i64>, DivisionError> {
let numbers = [27, 297, 38502, 81];
let division_results = numbers.into_iter().map(|n| divide(n, 27));

}

Explanation​

Without using the collect method, converting an iterator of Result types into a collection looks like this:

fn result_with_list() -> Result<Vec<i64>, DivisionError> {
let numbers = [27, 297, 38502, 81];
let division_results = numbers.into_iter().map(|n| divide(n, 27));

let mut results = Vec::new();
for result in division_results {
results.push(result?);
}

Ok(results)
}

The entire implementation above can also be written using the collect method. When converting an iterator of Result types into a collection, collect keeps accumulating Ok values, and if an Err is encountered, it terminates immediately and returns the Err. Furthermore, since the type can be inferred from the function's return type, simply writing division_results.collect() is sufficient.

fn result_with_list() -> Result<Vec<i64>, DivisionError> {
let numbers = [27, 297, 38502, 81];
let division_results = numbers.into_iter().map(|n| divide(n, 27)).collect();
division_results.collect()
}

21.3 Export Macros​

How to use macros defined in a module from other modules.

Problem​

How can you use the my_macro macro defined inside the macros module from the main function?

// TODO: Fix the compiler error without taking the macro definition out of this
// module.
mod macros {
macro_rules! my_macro {
() => {
println!("Check out my macro!");
};
}
}

fn main() {
my_macro!();
}

Explanation​

There are two approaches, and the official solution uses the older style.

Older Style​

Using the macro_use attribute.

#[macro_use]
mod macros {
macro_rules! my_macro {
() => {
println!("Check out my macro!");
};
}
}

fn main() {
my_macro!();
}

Newer Style​

Using use. With this approach, the macro can be used even on lines above the macro definition.

mod macros {
macro_rules! my_macro {
() => {
println!("Check out my macro!");
};
}
pub(crate) use my_macro;
}

fn main() {
macros::my_macro!();
}