In this exercise, we will implement our first tiny library: Shapes. Our Shapes library lets us work with basic geometric shapes in our Rust applications.

You will learn:

  • How to write a simple Rust library

  • How to create struct and enum types

  • How to write functions that take those types as arguments

  • How to use match to discriminate between variants of an enum type

Task

  1. Use Cargo to create a new project called shapes

  2. Define some or more of the following types:

    struct Square {
        // ...
    }
    
    struct RightAngleTriangle {
        // ...
    }
    
    struct Circle {
        // ...
    }
  3. Implement the following methods for each type:

    • fn new(…​) → Foo

    • fn area(self: &Foo) → …​

    • fn perimeter(self: &Foo) → …​

  4. Write some unit tests

  5. Write some enum Shape { …​ } which abstracts over those shapes.

  6. Implement the area and perimeter functions for your Shape type.

Help

This section gives partial solutions to look at or refer to.

In general, we also recommend to use the Rust documentation to figure out things you are missing to familiarise yourself with it. If you ever feel completely stuck or that you haven’t understood something, please hail the trainers quickly.

Getting Started

Create a new library Cargo project, check the build and see if it runs:

$ cargo new --lib shapes
$ cd shapes
$ cargo run

Creating a Type

Each of your shape types (Square, Circle, etc) will need some fields (or properties) to identify its geometry. Use /// to add documentation to each field.

/// Describes a human individual
struct Person {
    /// How old this person is
    age: u8
}

Functions that take arguments

Does your function need to take ownership of the shape in order to calculate its area? Or is it sufficient to merely take a read-only look at the shape for a short period of time?

You can pass arguments by reference in Rust by making your function take x: &MyShape, and passing them with &my_shape.

You can also associate your function with a specific type by placing it inside a block like impl MyShape { …​ }

impl Pentagon {
    fn area(self: &Pentagon) -> u32 {
        // calculate the area of the pentagon here...
    }
}

A Shape of many geometries

You can use an enum to provide a single type that can be any of your supported shapes. If we were working with fruit, we might say:

struct Banana { ... }
struct Apple { ... }

enum Fruit {
    Banana(Banana),
    Apple(Apple),
}

Which shape do I have?

A match expression will let you determine which variant your enum currently has. Again, using fruit as an example:

enum Fruit {
    Banana(Banana),
    Apple(Apple)
}

impl Fruit {
    fn some_function(self: &Fruit) {
        match self {
            Fruit::Banana(banana) => { ... }
            Fruit::Apple(apple) => { ... }
        }
    }
}

Remember, a match expression is all about pattern matching, not testing for equality.

I need a Pi, and a Square Root

The f32 type also has its own module in the standard library called std::f32. If you look at the docs, you will find a defined constant for Pi, and some useful functions for performing mathematical functions like square-root.

let x: f32 = 25.0;
let y = x.sqrt();
let z = x * x * std::f32::consts::PI;