Written by Peter Varo & Tom Steavenson

Rust exercises for programmers

Data engineering

10 minutes

Try out these Rust exercises to practice and sharpen your Rust language programming skills.

In the past year, our Engineering team at hyperexponential launched a Rust lecture series for hyperexponential engineers, encouraging our teams to learn and get familiar with the programming language. We ran 30 hour-long lectures, following the structure of The Rust Book, and included homework exercises. New joiners to hyperexponential have enjoyed going through the recordings and submitting their solutions to the exercises for review by their peers.

Exercises are often the thing that's missing from programming language learning resources, so we thought to contribute these to the world! In this blog, we'll take you through 8 exercises to test your Rust knowledge - let's get into it!

Warm-up rust practice exercises

These are some small warm-up exercises that you can write as a single function. The Rust Playground is a good place to write these up quickly.

  1. Write a function to return the nth Fibonacci number

  2. Write a function to convert between Celsius and Fahrenheit

  3. Write a function to generate the lyrics to “The Twelve Days Of Christmas”

For each exercise, have cargo installed, and create a new project. Each exercise will provide you with a main.rs to get you started.

Note: Some of the early exercises are designed around having only covered the book up to the accompanying chapter. If you are more experienced in Rust, there may be easier ways to solve the problem with more advanced language knowledge. We’ll denote which chapters of the book the exercises correspond to, and you can decide whether to try and solve the problems with only the parts of Rust that have been covered up to that point or bring your full Rust knowledge to bare.

Exercise #1 - Rectangular

This exercise is based on Chapter 5 of the Rust Book (Structures).

In this exercise, you will build a small utility that calculates the area and perimeter of a rectangle from the width and height provided by the user.

Your program will draw a rectangle with the correct ratio on the screen (where the minimum screen width is 80 and the minimum height is 40), place the width and height properties on the rectangle, and the calculated values inside it.

Note: You can get creative when it comes to drawing the rectangle but one good way of doing it is with the box-drawing characters.

To get started, replace the main.rs of your new project with the code below. You can copy this code from the Rust Playground using this link.

And add clap to your dependencies in cargo.toml:

Example output:

Draw wide or tall rectangles - You could use other characters to represent edge-cases, e.g. "half" lines and dashed lines to indicate visual trimming:

Bonus

If you like a bit of an extra challenge and you wish to practice responsibility separation more, you could add any of the following extra lines to the definition of the Arguments struct:

And make your program draw the rectangles with bold lines and/or ASCII only character via the --bold-lines and --ascii-only command line parameters.

Exercise #2 - Gringotts

This exercise is based on Chapter 6 of the Rust Book (Enums and Pattern Matching). For more on pattern matching, also read Chapter 18.

In this exercise, you will build a small utility for Muggles to convert between the different coins of the wizarding currency of Great Britain.

To get started, replace the main.rs of your new project with the code below. You can copy the code from the Rust Playground using this link.

And add clap to your dependencies in cargo.toml:

Example output:

Exchange rate:

Bonus

If you like a bit of an extra challenge you could add any of the following extra lines to the definition of the Arguments struct:

For Muggles it might make more sense to convert from their currency to the wizard one, after all, that's what they are using on a daily basis. The addition to from and to is only in the help text: both should accept more currencies now, it's up to you how many currencies you wish to support. Have a look at the approximate exchange rate and cherry pick your favourites.

You can also use this gringotts utility in conjunction with other programs. For instance, you might want to pipe the output of this program to another one and in that scenario having the fancy, formatted output is just making things much more complicated for the subsequent user of the conversion result. That's where the --simple-output flag comes in e.g.

Exercise #3 - Bye Bob

This exercise is based on Chapter 8 of the Rust Book (Common Collections).

In this exercise, you will build a text interface that allows you to track and query employees within a company and its departments.

To get started, replace the main.rs of your new project with the code below. You can copy the code from the Rust Playground using this link.

Example output:

Your text interface should support 4 commands:

The exact form of these commands is up to you, the example output above is just a suggestion.

Bonus

- Add a 5th command, Transfer to transfer an employee from one department to another.

- Add additional sorting options when executing List or Get commands, including:

  • Reverse alphabetically

  • Seniority, based on insertion order

Exercise #4 - Can I cook it?

This exercise is based on Chapter 9 of the Rust Book (Error Handling).

You have a very small kitchen, with very limited storage space, so you can only store one (or small portion) of each thing at any given time. But you also want to figure out what you can cook from them. This is where the can-i-cook-it utility comes in: it helps you find out if you have the necessary ingredients for the meal(s) you're planning to prepare.

Things you can store

  • Cod (fish)

  • Haddock (fish)

  • Peas

  • Potatoes

  • Eggs

  • Shiitake (mushroom)

  • Maitake (mushroom)

  • Garlic

  • Spinach

  • Cheddar (cheese)

  • Parmesan (cheese)

  • Macaroni (pasta)

  • Spaghetti (pasta)

  • Onion

  • Tomato

  • Pepper

Recipes you know

- Mushy Peas

Ingredients: Peas

- Fish & Chips

Ingredients: Fish, potato

- Mushroom Spinach Omelette

Ingredients: Egg, mushroom, spinach, onion, cheese

- Mac & Cheese

Ingredients: Cheese, pasta

To get started, replace the main.rs of your new project with the code below. You can copy the code from the Rust Playground using this link.

And add clap to your dependencies in cargo.toml:

Example usage:

You look at your supplies in the kitchen and you see that you have a cod in the fridge and frozen peas in the freezer, but you cannot think of anything else to eat than a tasty battered fish and some nice crispy chips:

Fortunately, it pops into your mind that you went to shopping this morning and you still have a bag of potatoes in your backpack:

But then your friend calls, saying they are in town for a few hours. You don't mind the company so you invite them over, however you realise you really don't want them to starve and watch you eating alone. But you don't have time to pop into the nearby store, so you ask your friendly, next-door neighbour to borrow you whatever they have. They can spare you some eggs, a few shiitake mushrooms, garlic, and a bag of spinach:

Bonus

The glue-code provided can be overridden if you wish to push the responsibility of validation to clap (because you figured out a way to represent the input in a better way than just a Vec of Strings), you could do that with its derive API as well.

Exercise #5 - Custom collects

This exercise is based on Chapter 10 of the Rust Book (Generic Types, Traits, and Lifetimes).

Write a library to combine a Vec<Result<T, E>> into a single Result<Vec<T>, F> where the returned error type communicates the details of all the errors in the Vec.

The returned type F must implement std::error::Error but you may need to place further requirements on F.

You must decide what constraints to place on E in order for all the error information to be collected into F. These can be traits you define.

src/lib.rs defines this challenge in code. Complete the collect_errors function filling in the TODOs.

To get started, replace the lib.rs of your new project with the code below. You can copy the code from the Rust Playground using this link.

Tip: Minimise constraints (make it as generic as possible) without sacrificing functionality.

Exercise #6 - Iterators

This exercise is based on Chapter 13.2 of the Rust Book (Iterators)

Choose a mathematical series, e.g: The Fibonacci Sequence, The Colatz Sequence, etc., and write an iterator to deliver it.

Are there limits to how far the sequence can go?

Exercise #7 - Collecting all the errors

This exercise is based on Chapter 13.2 of the Rust Book (Iterators)

The problem

Say we've got an application that takes data from the user via a series of YAML files. When there's a mistake in the file, either an invalid YAML or an incorrect data structure, we present a validation error to our users.

Our users have explicitly asked us to provide more than one error at a time, when they've made multiple errors. Ideally, they'd like to know all the places they've made an error. They don't want to have to run our application multiple times to fix each error. Our users are techies and are used to language compilers that provide them with multiple errors and warnings at once.

In our application, we find ourselves iterating across the files and through the YAML data (if it parses as YAML ok), to validate the data. From our learnings, we know if you have an iterator of Result<T, E>, you can use Iterator::collect to see if any of them failed:

But this is no good to our users. They want to see all their errors!

Editing the above to:

Gives us this compile error:

And looking at how std::iter::FromIterator is implemented for std::result::Result we can see the issue:

The error type E that's in the Err variant of each Result inside the iterator is the same type as the Err variant of the returned Result.

If instead, it was something like:

We might get our "collecting all the errors". Unfortunately "the orphan rule" means we can't just implement this in our own crate.

Challenge

Come up with a way that we can "collect all the errors" from an iterator of Results.

“Ideas for a Solution”, below, contains some thoughts that might help get you started. For maximum challenge, don't read it.

There's not a right or wrong answer on how to do this, or what restrictions to place on the solution. The real question is "does this produce something that's usable and ergonomic to callers?" They could always not use Iterator::collect and manually implement this conversion every time they need to do it, but we're looking for something that elides that. Create a library/crate that provides this abstraction.

Ideas for a solution - Don’t read this if you don’t want any hints for solving this problem

We can't implement a new std::iter::FromIterator implementation for std::result::Result, because of "orphan rule".

We could create our own type, implement the FromIterator that we need, then provide a conversion from our type into std::result::Result.

We'd have to document for calling code to call an extra .into() after collection, but that could be still quite ergonomic:

If we go down this road, there are a couple of things to consider:

  • How and when is the "collected errors" type F specified by the user?

  • It's annoying when the inner type of Result::Err doesn't implement std::error::Error, or have a pre-made conversion into a type that implements std::error::Error (makes ? not very useful). How should we ensure this?

Our own collector trait - Instead of trying to build something that will allow us to get what we need from Iterator::collect, we could create our own "collector trait", and implement it for a generic case of an iterator of results. You can copy the code from the Rust Playground using this link.

(I can't promise all those generics and trait bounds are right - they're only indicative, and you can choose different constraints to solve the problem.) This solution might build on top of what you learned in the previous error collection exercise. There will be many of the same considerations:

  • What extra restrictions on the types (trait bounds) will be necessary?

  • Do you want the F: std::error::Error constraint? Is this what's right for the users of this trait?

  • Do you want F to be generic and user defined? Or would it be better to provide our own type with some easy conversions into other errors?

Exercise #8 - Maybe owned string

This exercise is based on Chapter 15 of the Rust Book (Smart Pointers).

You are writing a naive JSON parsing library. Your use case only wants to receive its data in strings, so you're offering an API like the following:

So for example, the following JSON:

Would be parsed into:However, when testing and profiling your library, you notice that you have some performance problems. In particular, you seem to be allocating a lot of String structs. You realise that most of the time when reading a key or value in JSON, the bytes in the source string are exactly the same as what will be present in your data structure, but you are copying the data over anyway. Only when you are serializing data types (number to string) or unescaping the contents of the string do you need to actually allocate a String!

You want to write a structure that represents either an owned string (String) or a borrowed one (&str) to eliminate your allocation problems. This structure should have the following characteristics:

  • Implement Deref<Target = str>, Hash, PartialEq, Debug

  • Offer the following public methods:
    undefinedundefinedundefinedundefined

  • Some unit tests, if you're feeling fancy

You should also modify the JsonValue struct and parse_json methods as appropriate.

Note: Please do not actually implement a full JSON parser, that's just part of the context where such smart pointer could be useful. (Or do, you know, it's your free time after all).

To get started, replace the lib.rs of your new project with the code below. You can copy the code from the Rust Playground using this link.

That's it for today! We hope you found this helpful! If you'd like to see more from the hyperexponential Engineering team, consider checking our careers page for our latest openings :)