Week 5 exercises: Traits and Generics
You’re now past the halfway point of the quarter!! 🎉🎉🎉 By this point, you’ve learned so much about program analysis, memory safety, and code organization paradigms. You’re now more than capable of writing some pretty sophisticated code!
Purpose
This week, we will be working through some traits and generics syntax, which is handy for writing clean and well-organized code. This is a bit difficult to exercise in a one-week assignment, because it’s most handy in a large codebase, and we don’t want to have you sift through one. Instead, we thought we’d give you some lighter practice working with some types modeling plants and animals.
Due date: Tuesday, May 4, 11:59pm (Pacific time)
Ping us on Slack if you are having difficulty with this assignment. We would love to help clarify any misunderstandings, and we want you to sleep!
Getting the code
You should have received an invite to join this week’s Github repository. If you didn’t get an email invite, try going to this link:
https://github.com/cs110l/week5-YOURSUNETID
You can download the code using git
as usual:
git clone https://github.com/cs110l/week5-YOURSUNETID.git week5
Milestone 1: Reading the starter code
In this assignment, we’re going to work through the process of defining types that represent different kinds of plants. Maybe this is part of an app that reminds you when to water your plants, or something like that.
The starter code defines types for two neat plants I just recently learned about.
First, the sensitive plant
(it’s actually called that!!) is a plant that closes its leaves when touched.
It’s really wild to see! Our
SensitivePlant
type defines a poke
method along with is_open
to check
whether the leaves are open, and a water
method along with needs_watering
.
Second, the “string of turtles” has leaves that look like turtle shells growing
on stringy vines. Our StringOfTurtles
type has a num_turtles
method along
with similar water
and needs_watering
methods.
There isn’t a real main
function here, since we’re just defining and
implementing types in this assignment, but you’re free to add any code you like
if you want to play around with things.
We have included a series of commented-out unit tests, which you will uncomment as you work through the assignment.
Milestone 2: Printing our collection
We have nice SensitivePlant
and StringOfTurtles
types defined, but there
isn’t all that much we can do with them yet. One common thing we might want to
do is to be able to print out objects of these types.
In Rust, there are two common ways to print things: using the Display
trait,
which generates a clean, human-friendly representation of an object, or the
Debug
trait, which shows a verbose representation with any information that
may be useful for debugging.
For this assignment, we want you to implement Debug
on the two plant types.
Rust makes it extremely easy to do this using the derive
macro:
#[derive(Debug)]
pub struct SensitivePlant {
...
}
Types implementing Debug
can be printed using the {:?}
format specifier
(e.g. println!("{:?}", thing_to_print)
).
If you feel inclined, you’re also welcome to try implementing
Display
,
although there is no need for you to do this. This would allow you to print the
objects using the normal {}
format specifier.
After this milestone, uncomment the test_printing_plants
test function and
run cargo test
. The tests should pass cleanly with no errors.
Milestone 3: Caring for our plants
Let’s define a function that takes a reference to a plant and waters it if it needs water:
pub fn check_on_plant(plant: &mut SensitivePlant) {
if plant.needs_watering() {
println!("Watering the {}...", type_name::<SensitivePlant>());
plant.water();
println!("Done!");
} else {
println!("The {} looks good, no water needed!", type_name::<SensitivePlant>());
}
}
Unfortunately, this code only works with SensitivePlant
s. How can we make
this accept other plants as parameters as well?
We need to ensure that whatever plant
we take has at least two things:
- It needs to have a
needs_watering
method, which returns abool
indicating whether the plant needs to be watered - It needs to have a
water
method, which waters the plant
To do this, you’ll want to define your own trait, such that any type
implementing that trait has those two functions above. Then, you’ll want to
turn check_on_plant
into a generic function, using a trait bound to ensure
that the above conditions are met.
You have some flexibility in how you implement this, and you should consider the implications of the decisions you make. I can think of at least two ways to design this trait:
- You can require types to provide
last_watered()
andideal_watering_frequency()
functions, and then providing a default implementation ofneeds_watering()
that uses these functions. This may lead to decreased code repetition, but it might decrease flexibility in case you decide to change howneeds_watering
works, e.g. maybe it should consider ambient temperature/humidity, amount of sunlight, etc. (On the flipside, you could argue that providing a default implementation is fine and you can always override the default implementation for types that need a differentneeds_watering
behavior. That’s a good argument too.) - You can declare the trait such that
needs_watering()
andwater()
are required for any type implementing the trait, but no default implementation is provided. This will lead to increased code repetition; the implementations ofneeds_watering
are very similar forStringOfTurtles
andSensitivePlant
, but you’d need to implementneeds_watering
separately for each type. However, this might provide increased flexibility if you decide to change the way thatneeds_watering
works for certain plants in the future.
When you’re done, uncomment the test_checking_on_plants
test, and make sure
that cargo test
passes.
Additional (optional but recommended) challenge: The implementation of
test_checking_on_plants
is pretty messy, with a lot of copy and paste. Try
cleaning up the code by storing the plants in a vector using trait objects (see
end of lecture 9 and example
code).
Then, you can loop over that vector, calling check_on_plant
and assert_eq!
.
Milestone 4: Introducing animals
Let’s bring some cats and dogs into our garden! We can define a Cat
type:
pub struct Cat {
last_fed: DateTime<Local>,
}
impl Cat {
pub fn new() -> Cat {
Cat { last_fed: Local::now() }
}
pub fn last_fed(&self) -> DateTime<Local> {
self.last_fed
}
pub fn needs_feeding(&self) -> bool {
(Local::now() - self.last_fed()).num_hours() > 12
}
pub fn feed(&mut self) {
println!("...om nom nom...");
self.last_fed = Local::now();
}
}
And a Dog
type:
pub struct Dog {
last_fed: DateTime<Local>,
}
impl Dog {
pub fn new() -> Dog {
Dog { last_fed: Local::now() }
}
pub fn last_fed(&self) -> DateTime<Local> {
self.last_fed
}
pub fn needs_feeding(&self) -> bool {
(Local::now() - self.last_fed()).num_hours() > 8
}
pub fn feed(&mut self) {
println!("...om nom nom...");
self.last_fed = Local::now();
}
}
Similar to the last milestone, we want to declare a check_on_pet
function to
take care of our pets:
pub fn check_on_pet(pet: &mut Cat) {
if pet.needs_feeding() {
println!("Feeding the {}...", type_name::<Cat>());
pet.feed();
println!("Done!");
} else {
println!("The {} looks happy and full!", type_name::<Cat>());
}
}
Let’s make this generic! Define a new trait with needs_feeding()
and
feed()
, and fix up check_on_pet
so that it works for any pet that needs
food. Then, uncomment the test_checking_on_pets
test and make sure everything
looks good.
Milestone 5: Carnivorous plants
Fun fact: Carnivorous plants are native to regions with poor soil, and consuming insects is an adaptation to help them derive additional nutrients. As such, carnivorous plants need to eat!
Let’s imagine that we live in a perfect utopia that somehow has no insects buzzing around. We import flies for our carnivorous plants. This sounds so silly as I’m typing this, but for the sake of a contrived example, bear with me. 😁
Let’s define our VenusFlyTrap
:
pub struct VenusFlyTrap {
last_watered: DateTime<Local>,
last_fed: DateTime<Local>,
}
impl VenusFlyTrap {
pub fn new() -> VenusFlyTrap {
VenusFlyTrap { last_watered: Local::now(), last_fed: Local::now() }
}
pub fn last_watered(&self) -> DateTime<Local> {
self.last_watered
}
pub fn last_fed(&self) -> DateTime<Local> {
self.last_fed
}
pub fn needs_watering(&self) -> bool {
(Local::now() - self.last_watered()).num_hours() > 10
}
pub fn water(&mut self) {
self.last_watered = Local::now();
}
pub fn needs_feeding(&self) -> bool {
(Local::now() - self.last_fed()).num_hours() > 8
}
pub fn feed(&mut self) {
println!("...snap!!");
self.last_fed = Local::now();
}
}
Since a VenusFlyTrap
needs water and needs to be fed, modify it so that it
works with both check_on_plant
and check_on_pet
. Uncomment the
test_venus_fly_trap
and make sure that cargo test
still runs clean!
Milestone 6: Weekly survey
Please let us know how you’re doing using this survey.
When you have submitted the survey, you should see a password. Put this code in
survey.txt
before submitting.
Submitting your work
As with last week, you can commit your progress using git
:
git commit -am "Type some title here to identify this snapshot!"
In order to submit your work, commit it, then run git push
. This will upload
your commits (snapshots) to Github, where we can access them. You can verify
that your code is submitted by visiting
https://github.com/cs110l/week5-yourSunetid and browsing the code there. You
can git push
as many times as you’d like.
Grading
Each milestone will be worth 17% of the grade. You’ll earn the full credit for each piece if we can see that you’ve made a good-faith effort to complete it.