Lab 2: Baking Cookies

Due February 21, 11:59pm

We just spent the last class discussing how Rust's ownership model works. In this lab we will be exploring this idea by baking some cookies.

Grading Rubric

  • Code is formatted properly using cargo fmt: 5%
  • Code passes cargo clippy without warnings: 20%
  • Implementation of Dough, Cookie, and Oven: 25%
  • Responses in questionnaire.md: 50%

50% of the grade for this lab is in the questionnaire. Allocate your time appropriately!

Learning Objectives

  • Practice working with the Rust ownership model.
  • Explore how Rust's ownership model can catch common bugs.

Table of Contents

Structs

Skim or read this section of the Rust book to familiarize yourself with Rust's syntax for structs. I'd like to particularly draw your attention to this sentence.

Methods can take ownership of self, borrow self immutably as we've done here, or borrow self mutably, just as they can any other parameter.

They don't give any examples of taking ownership of self or borrowing self mutably, so I will extend their examples here. Let's say we wanted to add an expand_width method to the Rectangle struct they had. We could do that like so:

#![allow(unused)]
fn main() {
impl Rectangle {
    fn expand_width(&mut self, amount: u32) {
        self.width += amount;
    }
}
}

This time, note that &mut self is shorthand for self: &mut Self which is itself an alias for self: &mut Rectangle.

Let's say we also had a Square struct defined simply as shown below.

#![allow(unused)]
fn main() {
struct Square {
    width: u32,
}
}

Then we might want a method for the Rectangle struct to convert it into a Square with the same width. In this case, we want to take ownership of the Rectangle and return a new Square. This method would look like the following.

#![allow(unused)]
fn main() {
impl Rectangle {
    fn square_of_same_width(self) -> Square {
        Square {
            width: self.width,
        }
    }
}
}

Now self is shorthand for just self: Self, meaning self owns the struct, meaning the struct is dropped when this function returns! This means that the following code does not compile.

#![allow(unused)]
fn main() {
let rect = Rectangle {
    width: 10,
    height: 20,
};

let square = rect.square_of_same_width();
println!("{} = {}", rect.width, square.width);
}

I encourage you to ponder why it's a good thing that Rust doesn't allow this code to compile even though, in this circumstance, it seems perfectly safe.

Baking Cookies

With that out of the way, we can get to the good stuff. In this lab, we will be modeling cookies and the baking thereof with various structs and methods. Throughout this lab, we will make three structs: one to represent raw cookie dough, another to represent a cookie, and another to represent an oven. We'll then write some programs to experiment with converting dough into baked cookies, allowing Rust's ownership model to guide us along.

This lab is very exploratory and the purpose is to get hands on experience with Rust's ownership model and borrowing, so there's not a ton of coding or tests to turn in. However, that doesn't mean you should leave it to the last minute!

The Dough type

Make a Dough struct containing a flavor field of type String. Then implement the following methods:

  • new, which takes in a &str and returns a Dough. An example for how to do this can be found below.
  • Some reasonable constructors for popular flavors. For example, a chocolate_chip method which creates a Dough that's chocolate chip flavored without having to pass in the string "Chocolate chip" manually.

As a reminder for how to implement methods, here's how you might implement and use the new method:

#![allow(unused)]
fn main() {
// Dough definition omitted

impl Dough {
    fn new(flavor: &str) -> Dough {
        Dough {
            flavor: flavor.to_string(),
        }
    }

    // other methods here...
}
}

Unlike the other methods we've seen in class, this one has no &self parameter, but instead has a &str parameter. This allows us to create Dough values with the following code:

#![allow(unused)]
fn main() {
let oatmeal_raisin = Dough::new("Oatmeal raisin");
}

Here, we put the name of the type, Dough, followed by ::, followed by the method name, so Dough::new. Note that the name new in Rust isn't special: it's just a convention for methods that return an instance of the type. If we wanted, we could have called it with_flavor or something else.

Next, make a Cookie struct. It should have a field flavor of type String, and a field crispiness of type u32. Then implement the following methods:

  • new, which should takes an owned Dough and returns a Cookie with the same flavor and a crispiness of 0. Remember, if Dough has ownership of the flavor String, you can transfer ownership to the Cookie to avoid allocating another String.
  • is_raw, which borrows self and returns a bool. This will be used to check if the Cookie is raw, i.e. the crispiness is 0.
  • is_overcooked, which borrows self and returns a bool. This will be used to check if the Cookie is overcooked, which we'll say is when crispiness > 4.
  • eat, which will take ownership of the Cookie and prints a message that changes based on the flavor and if the cookie is overcooked, raw, or somewhere in between, which you can use the methods you already created to test for. Feel free to be creative and have fun here; we certainly did in testing this lab. (Idea: maybe chocolate chip cookies taste best nice and gooey with a crispiness of 1, but snickerdoodles are best crispier with a crispiness of 4.)

The Oven type

Make an Oven struct with a field tray of type Vec<Cookie>. Then implement the following methods:

  • new, which takes no arguments and returns an Oven with the tray field initialized to an empty Vec using Vec::new().
  • add_raw_cookie, which borrows self (mutably or immutably?) and takes an owned Dough, converts it into a Cookie with Cookie::new, and adds it to the tray field using .push(x).
  • wait, which borrows self (mutably or immutably?) and loops through the tray using .iter_mut() to increment the crispiness of all Cookies by 1. Look at the linked documentation for examples on how to use.
  • inspect_cookie, which immutably borrows self and takes an index of type usize and returns a reference to the Cookie in the tray at index using &self.tray[index].
  • remove_tray, which takes the owned Oven and returns a Vec<Cookie>.

Note that after you remove a Vec<Cookie> from the Oven, you can use .remove(i) to get a Cookie from the vector at a particular index, or you can just iterate through each Cookie with:

#![allow(unused)]
fn main() {
for cookie in oven.remove_tray() {
    // do stuff with the cookie
}
}

Questionnaire

The last part of this assignment is to fill our several short response questions in questionnaire.md.

Feedback

Please fill out this short feedback form.

Submitting

Once you're finished, be sure to verify that:

  • cargo fmt has been run
  • cargo clippy passes
  • cargo test passes

Then you can push your changes with the usual: git add ., git commit -m "your message", and git push.

Congratulations on finishing!