Getting Started: Fibonacci

To quickly get started with Rust, we'll begin with a classic: the Fibonacci sequence.

Project Creation

❎ Create a new project with the command cargo new fibo. This will create a new fibo directory with a corresponding binary project (you could have used --lib to create a library project). Navigate to this directory.

The project is organized as follows:

  • At the root, Cargo.toml contains project information (name, author, etc.). This file will also include dependencies used by this project.
  • Also at the root, Cargo.lock will contain, after compilation, information about the exact versions of dependencies used for that compilation, allowing the exact compilation conditions to be reproduced if necessary.
  • src/ contains the project code.

All these files and directories are intended to be added to your version control system (git in our case). By default, if you are not already in a git repository, cargo also creates an empty git repository in the target directory along with a .gitignore file. You can change this behavior with the --vcs option (see cargo help new).

After compilation, a target directory will contain object files and binaries. It should be ignored by the version control system (it is in the .gitignore file created by cargo).

Compile the project using the cargo build command. By default, a debug version, much slower but more easily debuggable, will be built and can be found in target/debug/fibo. Run the program, and for now, observe that it displays "Hello, world!". This corresponds to the code in src/main.rs.

You can also compile and execute (in case of success) the program in a single command: cargo run.

Note: You can build or run release versions using cargo build --release and cargo run --release.

Recursive Implementation of the fibo Function

❎ Implement the fibo function recursively with the following prototype:

#![allow(unused)]
fn main() {
fn fibo(n: u32) -> u32 {
    // TODO
}
}

We should have fibo(0) = 0, fibo(1) = 1, fibo(n) = fibo(n-1) + fibo(n-2). Remember that if expressions return a value; you don't need to use return explicitly in your code.

In the main function, create a loop from 0 to 42 (inclusive) that displays:

fibo(0) = 0
fibo(1) = 1
fibo(2) = 1
fibo(3) = 2
fibo(4) = 3
fibo(5) = 5

up to fibo(42) = 267914296.

You can compare execution speeds in debug and release modes.

Iterative Implementation

❎ Reimplement the fibo function with the same signature but iteratively.

For this, you will probably need:

  • to declare variables
  • to declare mutable variables with mut
  • to create a loop in which you do not use the loop index; you can use _ as the loop index name to avoid compiler warnings

You may also return prematurely using return if the argument is smaller than 2, for example.

Checking Calculations

Change the maximum limit from 42 to 50. Notice what happens between fibo(47) and fibo(48). Do you understand what is happening?

We have several ways to fix this issue:

  • increase the size of integers and use u64 instead of u32
  • use saturated arithmetic, which ensures that in case of exceeding a boundary (lower or upper), the value of that boundary will be returned
  • use checked arithmetic, which signals an error if the operation results in a value that does not fit into the targeted size

Saturated Arithmetic

In the documentation for the u32 type, look for the saturating_add method.

❎ Replace the addition in your code with saturated addition. Run the program and compare.

Note: Remember that you can specify the type of numeric literals by suffixing them with a predefined type, such as 1u32.

The results are now monotonic but still not correct. They are limited by the maximum value of a u32, namely 232-1.

Checked Arithmetic

In the u32 type documentation, look for the checked_add method.

❎ Replace the (saturated) addition with a call to checked_add followed by an unwrap() call to retrieve the value from the option. Run the program and observe the runtime error.

Although the program crashes, at least it no longer displays incorrect values!

Displaying Only Correct Values

Change the fibo function prototype like this:

#![allow(unused)]
fn main() {
fn fibo(n: u32) -> Option<u32> {
    // TODO
}
}

Return None if it is not possible to represent the result as a u32, or Some(result) when it fits into a u32.

❎ After making the above modifications, modify the main program to exit the loop as soon as it is not possible to display the result.

You can advantageously use:

  • match
  • if let Some(…) = … { }

Use of Crates

A crate is a collection of functionalities. It can be of two kinds: binary or library. When you created your project, you created a binary crate with the project name (fibo), and its source code is in src/main.rs.

You can import crates to use the functionalities they offer, either from local projects or from remote sources. The Rust ecosystem provides a central repository crates.io that gathers many crates (but you can use other repositories). The cargo utility allows you to easily fetch these crates, use them in your projects, and track their updates. It also allows you to easily publish your own crates.

We propose using the clap crate to add the ability to pass arguments and options on the command line to our fibo program.

❎ Add the following two lines to the Cargo.toml file:

[dependencies]
clap = { version = "4.1.14", features = ["derive"] }

This indicates that our project requires the clap crate, in version 4.1.4 or later, up to version 5.0.0 exclusive (more information in cargo's documentation on how to specify these version numbers). It also indicates that we want to use the derive feature of clap which is not enabled by default. This feature allows the use of #[derive(Parser)].

Now, in main.rs, import the structures from the crate that you need to use:

#![allow(unused)]
fn main() {
use clap::Parser;
}

❎ Using the clap documentation, modify your application to work according to the following schema:

Compute Fibonacci suite values

Usage: fibo [OPTIONS] <VALUE>

Arguments:
  <VALUE>  The maximal number to print the fibo value of

Options:
  -v, --verbose       Print intermediate values
  -m, --min <NUMBER>  The minimum number to compute
  -h, --help          Print help

(You may not have exactly the same display depending on the versions and features of clap, it's okay)

Note that specifying that you are using clap in Cargo.toml automatically and transitively fetches clap's dependencies and compiles them when building the application.

You can check the exact versions used in the Cargo.lock file mentioned earlier, allowing other users to exactly rebuild the same version of the program that you have built yourself.

❎ Use cargo clippy to run Clippy on your code, and apply the suggestions or change your code until Clippy does not complain anymore.

❎ Use cargo fmt to reformat your code according to the common Rust formatting conventions.

(we expect you to execute those steps every time you commit your code to the main branch or your repository)