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:
-
Change
optional_pointon line 11 to&optional_pointmatch &optional_point {
Some(p) => println!("Co-ordinates are {},{}", p.x, p.y),
_ => panic!("No match!"),
} -
Change
Some(p)on line 12 toSome(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,
cmpis a method available on types that implement theOrdtrait. Implementing theOrdtrait 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!();
}