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
, andOven
: 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: Basic overview of structs in Rust.
- Baking Cookies: Overview of the lab.
- The
Dough
type: Specification for a struct representing dough. - The
Cookie
type: Specification for a struct representing cookies. - The
Oven
type: Specification for a struct representing an oven.
- The
- Questionnaire: Answer some brief questions about Rust.
- Feedback: Give us your feedback!
- Submitting: How to submit your lab.
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
, borrowself
immutably as we've done here, or borrowself
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 aDough
. 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 aDough
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.
The Cookie
type
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 ownedDough
and returns aCookie
with the same flavor and acrispiness
of 0. Remember, ifDough
has ownership of the flavorString
, you can transfer ownership to theCookie
to avoid allocating anotherString
.is_raw
, which borrowsself
and returns abool
. This will be used to check if theCookie
is raw, i.e. thecrispiness
is 0.is_overcooked
, which borrowsself
and returns abool
. This will be used to check if theCookie
is overcooked, which we'll say is whencrispiness > 4
.eat
, which will take ownership of theCookie
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 anOven
with thetray
field initialized to an emptyVec
usingVec::new()
.add_raw_cookie
, which borrowsself
(mutably or immutably?) and takes an ownedDough
, converts it into aCookie
withCookie::new
, and adds it to thetray
field using.push(x)
.wait
, which borrows self (mutably or immutably?) and loops through thetray
using.iter_mut()
to increment the crispiness of allCookie
s by 1. Look at the linked documentation for examples on how to use.inspect_cookie
, which immutably borrows self and takes anindex
of typeusize
and returns a reference to theCookie
in thetray
atindex
using&self.tray[index]
.remove_tray
, which takes the ownedOven
and returns aVec<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 runcargo clippy
passescargo test
passes
Then you can push your changes with the usual: git add .
, git commit -m "your message"
, and git push
.
Congratulations on finishing!