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.

Photo of string of turtles

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 SensitivePlants. 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:

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:

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.