14 Rustlings Options Solution

Options

From the Rustlings README

Type Option represents an optional value: every Option is either Some and contains a value, or None, and does not. Option types are very common in Rust code, as they have a number of uses:

  • Initial values
  • Return values for functions that are not defined over their entire input range (partial functions)
  • Return value for otherwise reporting simple errors, where None is returned on error
  • Optional struct fields
  • Struct fields that can be loaned or "taken"
  • Optional function arguments
  • Nullable pointers
  • Swapping things out of difficult situations

Further Information

Options1.rs

// options1.rs
// Execute `rustlings hint options1` or use the `hint` watch subcommand for a hint.

// I AM NOT DONE

// This function returns how much icecream there is left in the fridge.
// If it's before 10PM, there's 5 pieces left. At 10PM, someone eats them
// all, so there'll be no more left :(
fn maybe_icecream(time_of_day: u16) -> Option<u16> {
    // We use the 24-hour system here, so 10PM is a value of 22 and 12AM is a value of 0
    // The Option output should gracefully handle cases where time_of_day > 23.
    // TODO: Complete the function body - remember to return an Option!
    ???
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn check_icecream() {
        assert_eq!(maybe_icecream(9), Some(5));
        assert_eq!(maybe_icecream(10), Some(5));
        assert_eq!(maybe_icecream(23), Some(0));
        assert_eq!(maybe_icecream(22), Some(0));
        assert_eq!(maybe_icecream(25), None);
    }

    #[test]
    fn raw_value() {
        // TODO: Fix this test. How do you get at the value contained in the Option?
        let icecreams = maybe_icecream(12);
        assert_eq!(icecreams, 5);
    }
}

Our instructions in this exercise are to create the logic in the maybe_icecream body which should tell us how much ice cream is left depending on the time of day -- for example if it's 9pm there should be some ice cream but if it's 11pm there should be no ice-cream. Next, we have to fix the text so we get the value that is contained inside of the Option.

Options1.rs Errors

⚠️  Compiling of exercises/options/options1.rs failed! Please try again. Here's the output:
error: expected expression, found `?`
  --> exercises/options/options1.rs:13:5
   |
13 |     ???
   |     ^ expected expression

error[E0308]: mismatched types
  --> exercises/options/options1.rs:33:9
   |
33 |         assert_eq!(icecreams, 5);
   |         ^^^^^^^^^^^^^^^^^^^^^^^^
   |         |
   |         expected enum `Option`, found integer
   |         expected because this is `Option<u16>`
   |
   = note: expected enum `Option<u16>`
              found type `{integer}`
   = note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)
help: try wrapping the expression in `Some`
  --> /Users/desmo/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/macros/mod.rs:40:35
   |
40 |                 if !(*left_val == Some(*right_val)) {
   |                                   +++++          +

error: aborting due to 2 previous errors

There are some clear hints on how to fix this issues in the errors

Options1.rs solution

Looking at the errors the Rust compiler offers a direct hint for how to solve the test. Let's tackle that one first since it looks super easy the compiler we fix it by wrapping our value in Some() as suggested like this:

  #[test]
    fn raw_value() {
        // TODO: Fix this test. How do you get at the value contained in the Option?
        let icecreams = maybe_icecream(12);
        assert_eq!(icecreams, Some(5));
    }
}

This fixes the issue in our test, now on to the maybe_icecream function.

Maybe Ice Cream

So our instructions are to return the amount of ice cream left in the fridge. So from this we understand that we have to return a value as an Option<u16> by looking at the function signature. So, in the next sentence we get a big hint as to the type of function body we have to write because the instructions start with an "If" meaning we should be able to use a If statement in our function body. Let's see what that could look like. In pseudo code:

if time of day is < 10pm
	then 5
if time of day > 10pm
	then 0

Now, I started implementing this pseudo code but then realized that we actually have to pass 3 different test cases. Let's write this in the 24 hour clock system because it's easier.

  1. Before 22
  2. After 22 or 23
  3. Something else: 25

For item 3, we can see it explicitly stated in our test code:

    fn check_icecream() {
        assert_eq!(maybe_icecream(9), Some(5));
        assert_eq!(maybe_icecream(10), Some(5));
        assert_eq!(maybe_icecream(23), Some(0));
        assert_eq!(maybe_icecream(22), Some(0));
        assert_eq!(maybe_icecream(25), None);

So now, we know that we have to cover three different test cases Some(5), Some(0) and None. Let's implement this in Rust, keeping in mind the instructions to use a 24 clock system.

fn maybe_icecream(time_of_day: u16) -> Option<u16> {
    if time_of_day < 22 {
        Some(5)
    } else if time_of_day == 22 || time_of_day == 23 {
        Some(0)
    } else {
        None
    }
}

Using match

Another way to solve this problem in a more concise way would be to use a match statement and that could look something like this:

fn maybe_icecream(time_of_day: u16) -> Option<u16> {
    match time_of_day {
        0..=21 => Some(5),
        22..=23 => Some(0),
        _ => None,
    }
}

as you can see this is cleaner way of implementing a solution that handles all of our cases. Remember when using ranges you can use the ..= to include the element in the range so in our case 0...=21 means from midnight to 9pm, and 22..=23 means from 10pm to 11pm.

Options2.rs

// options2.rs
// Execute `rustlings hint options2` or use the `hint` watch subcommand for a hint.

// I AM NOT DONE

#[cfg(test)]
mod tests {
    #[test]
    fn simple_option() {
        let target = "rustlings";
        let optional_target = Some(target);

        // TODO: Make this an if let statement whose value is "Some" type
        if let Some(word) = optional_target {
            assert_eq!(word, target);
        }
    }

    #[test]
    fn layered_option() {
        let mut range = 10;
        let mut optional_integers: Vec<Option<i8>> = Vec::new();
        for i in 0..(range + 1) {
            optional_integers.push(Some(i));
        }

        // TODO: make this a while let statement - remember that vector.pop also adds another layer of Option<T>
        // You can stack `Option<T>`'s into while let and if let
        integer = optional_integers.pop() {
            assert_eq!(integer, range);
            range -= 1;
        }
    }
}

We have a couple of TODO's here one in the simple_option function that asks us to use a if let statement and then the next on the list asks for a while let statement with a reminder that using vector.pop also adds another layer of Option<T>, noting that we can 'stack' Option<T>'s when using while let and if let.

Options2.rs errors

⚠️  Compiling of exercises/options/options2.rs failed! Please try again. Here's the output:
error: expected one of `,`, `:`, or `}`, found `!`
  --> exercises/options/options2.rs:15:22
   |
14 |         word = optional_target {
   |                --------------- while parsing this struct
15 |             assert_eq!(word, target);
   |                      ^ expected one of `,`, `:`, or `}`

error: expected one of `,`, `:`, or `}`, found `)`
  --> exercises/options/options2.rs:15:36
   |
14 |         word = optional_target {
   |                --------------- while parsing this struct
15 |             assert_eq!(word, target);
   |                                    ^ expected one of `,`, `:`, or `}`

error: expected one of `.`, `;`, `?`, `}`, or an operator, found `{`
  --> exercises/options/options2.rs:29:43
   |
29 |         integer = optional_integers.pop() {
   |                                           ^ expected one of `.`, `;`, `?`, `}`, or an operator

error[E0425]: cannot find value `word` in this scope
  --> exercises/options/options2.rs:14:9
   |
14 |         word = optional_target {
   |         ^^^^
   |
help: you might have meant to introduce a new binding
   |
14 |         let word = optional_target {
   |         +++

error[E0574]: expected struct, variant or union type, found local variable `optional_target`
  --> exercises/options/options2.rs:14:16
   |
14 |         word = optional_target {
   |                ^^^^^^^^^^^^^^^ not a struct, variant or union type

error: aborting due to 5 previous errors

The error's don't give us a lot of a lot of hints in this case other letting us know that we probably want to introduce a new binding at line 14. So let's start there.

Options2.rs solution

Using if let bindings allow us to sort of use a match statement, that is less awkward in cases where there is only one correct outcome and we'd have to use _ to be exhaustive. which would lead to some cluncky code like in this example from Rust by Example

// Make `optional` of type `Option<i32>`
let optional = Some(7);

match optional {
    Some(i) => {
        println!("This is a really long string and `{:?}`", i);
        // ^ Needed 2 indentations just so we could destructure
        // `i` from the option.
    },
    _ => {},
    // ^ Required because `match` is exhaustive. Doesn't it seem
    // like wasted space?
};

So let's try using if let on our first TODO.

if let Some(word) = optional_target { // binding with `if let` wrapping `word` with Some
	assert_eq!(word, target);
}

With this we can save and see how our error's look.

⚠️  Compiling of exercises/options/options2.rs failed! Please try again. Here's the output:
error: expected one of `.`, `;`, `?`, `}`, or an operator, found `{`
  --> exercises/options/options2.rs:29:43
   |
29 |         integer = optional_integers.pop() {
   |                                           ^ expected one of `.`, `;`, `?`, `}`, or an operator

error: aborting due to previous error

Okay we went from 5 error's to 1, great let's work on that next part of the code with while let. Similar to if let using while let can make matching sequences easier to use in this case in a loop type setting. So let's try and implement the solution in the same way we did with the if let binding.

        // TODO: make this a while let statement - remember that vector.pop also adds another layer of Option<T>
        // You can stack `Option<T>`'s into while let and if let
        while let Some(integer) = optional_integers.pop() {
            assert_eq!(integer, range);
            range -= 1;
        }

Let's see what happens...wait there's an error

⚠️  Compiling of exercises/options/options2.rs failed! Please try again. Here's the output:
error[E0308]: mismatched types
  --> exercises/options/options2.rs:30:13
   |
30 |             assert_eq!(integer, range);
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^
   |             |
   |             expected `Option<i8>`, found `i8`
   |             expected because this is `Option<i8>`
   |
   = note: expected enum `Option<i8>`
              found type `i8`
   = note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)
help: try wrapping the expression in `Some`
  --> /Users/desmo/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/macros/mod.rs:40:35
   |
40 |                 if !(*left_val == Some(*right_val)) {
   |                                   +++++          +

error: aborting due to previous error

The compiler is telling us that it expects an Option<i8> but it's finding an i8...interesting. But wait let's look at that TODO comment: remember that vector.pop also adds another layer of Option<T> there it is. There's another layer of Option<T> that's created with vector.pop so does that mean that we have to wrap our Some() with Some()? Let's try.

while let Some(Some(integer)) = optional_integers.pop() {
            assert_eq!(integer, range);
            range -= 1;
        }

Success! This is our output

✅ Successfully tested exercises/options/options2.rs!

🎉 🎉  The code is compiling, and the tests pass! 🎉 🎉

Let's move on to our final Option exercise.

Options3.rs

// options3.rs
// Execute `rustlings hint options3` or use the `hint` watch subcommand for a hint.

// I AM NOT DONE

struct Point {
    x: i32,
    y: i32,
}

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

    match y {
        Some(p) => println!("Co-ordinates are {},{} ", p.x, p.y),
        _ => println!("no match"),
    }
    y; // Fix without deleting this line.
}

We're not getting a lot of instruction here but we're being told to fix this code without removing the y; on line 18. Let's take a look at the errors.

Options3.rs errors

⚠️  Compiling of exercises/options/options3.rs failed! Please try again. Here's the output:
error[E0382]: use of partially moved value: `y`
  --> exercises/options/options3.rs:18:5
   |
15 |         Some(p) => println!("Co-ordinates are {},{} ", p.x, p.y),
   |              - value partially moved here
...
18 |     y; // Fix without deleting this line.
   |     ^ value used here after partial move
   |
   = note: partial move occurs because value has type `Point`, which does not implement the `Copy` trait
help: borrow this binding in the pattern to avoid moving the value
   |
15 |         Some(ref p) => println!("Co-ordinates are {},{} ", p.x, p.y),
   |              +++

error: aborting due to previous error

In this case we get a big hint as to what is going on with the code and a very direct suggestion as to how to fix it it, let's see what happens if we implement the compiler suggestion.

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

    match y {
        Some(ref p) => println!("Co-ordinates are {},{} ", p.x, p.y), // adding `ref`
        _ => println!("no match"),
    }
    y; // Fix without deleting this line.
}

...and it compiles! Easy enough but let's take a deeper look at this, what is this ref and is it the same as &?

So first let's understand a little bit more about what is happening in this code and when we should use ref.

In Rust, the ref keyword is used in patterns to create a reference to a value instead of moving it. This is particularly useful when dealing with non-Copy types (types that don't implement the Copy trait) to avoid moving the value out of its original location, which would leave the original variable uninitialized or unusable.

When pattern matching on a non-Copy value, Rust requires us to either move the value or borrow it explicitly. The ref keyword helps us create a reference to the value instead of moving it.

In our error message, the compiler is complaining about a partially moved value, specifically the variable y, which is of type Point. Since Point does not implement the Copy trait, the pattern matching on Some(p) is attempting to move the value of p, causing a partial move of the variable y.

With ref p, we are now creating a reference to the Point value inside the Some variant, which avoids the move and allows you to access the x and y fields without any issues.

Remember that we need to use ref in the pattern for non-Copy types when matching on references. When using ref, you are borrowing the value, and if we don't use ref, you are moving the value out of the original variable. This is crucial to understand when dealing with non-Copy types in pattern matching.

Alright, but what was the difference between ref and & again?

Using ref in a pattern and using & in a pattern are related concepts but serve slightly different purposes in Rust.

Using ref in a pattern: - The ref keyword is used in pattern matching to create a reference to a value inside a pattern. It allows you to match on the reference to the value rather than moving the value out of the original variable. - It is particularly useful when dealing with non-Copy types, as it allows you to borrow the value instead of moving it, preserving the original variable's ownership. - ref can only be used in patterns, specifically in match arms or when destructuring tuples or structs.

Our example code using ref:

struct Point {
    x: i32,
    y: i32,
}

fn print_coordinates(point: Option<Point>) {
    match point {
        Some(ref p) => println!("Co-ordinates are {},{} ", p.x, p.y),
        None => println!("No point found!"),
    }
}

Using & in a pattern:

  • The & symbol in a pattern is used to destructure a reference. It allows you to match on the value being referenced instead of matching on the reference itself.
  • It is commonly used when working with references to allow pattern matching without consuming the reference.
  • & can also be used in match arms and when destructuring tuples or structs, just like ref.

Example using &:

fn print_coordinates_ref(point: Option<&Point>) {
    match point {
        Some(p) => println!("Co-ordinates are {},{} ", p.x, p.y),
        None => println!("No point found!"),
    }
}

In the first example, we use ref to match on the Point value by creating a reference &Point and avoid moving the original value out of the Option. In the second example, we take a reference as an argument to the function and use & to destructure the reference and match on the underlying Point value.

Both ref and & in patterns are essential tools when working with pattern matching and references in Rust. They allow you to control ownership and borrowing behavior while pattern matching on non-Copy types.

Conclusion

In conclusion, the Rust Option type is a fundamental concept that plays a crucial role in handling optional values in Rust code. It provides a safe and elegant way to deal with scenarios where a value may or may not be present. With Option, Rust encourages developers to handle potential absence of values explicitly, reducing the risk of null-related errors that are prevalent in other programming languages.

Throughout this blog post, we explored the versatility of Option and its various applications in Rust code. We learned that Option is commonly used for representing initial values, handling partial functions, reporting errors, working with optional struct fields, and more.

In addition to understanding the basics of Option, we also explored important Rust concepts like pattern matching, if let, and while let. These powerful language features allow us to handle Option values efficiently, making our code concise and expressive.

When working with non-Copy types, we encountered the use of ref in patterns, which creates a reference to the value, preventing it from being moved. This is particularly useful when matching on references to avoid unnecessary ownership transfers.

Furthermore, we compared ref with using & in patterns, noting that both have distinct purposes. While ref is primarily used for pattern matching, & is used for destructuring references.

In summary, mastering the Rust Option type and related language features empowers developers to write robust, safe, and efficient code. Embracing the principles of handling optional values in Rust not only improves code quality but also fosters better programming practices. By avoiding null-related issues, Rust's Option encourages developers to write more reliable software, making Rust a language of choice for projects requiring high-level safety and correctness guarantees.