Learn programming with Rust as first language

Introduction

Coding can be a daunting task to any beginner. In fact even at seniority levels, it still is for advanced concepts. As you get on your journey of learning how to code you’ll begin to find previous hard tasks as easy. At some point they will become so obvious that it gets hard to imagine how other people don’t understand them. But regardless, there will always be daunting tasks waiting for you. Programming is not about the destination, it is about the journey itself.

It’s not something that you learn, and you’re done. There’s almost an infinite amount of stuff that can be learned and used, as you gain more expertise, you’ll be able to specialize on certain types of programs and make them go the extra mile, which feels very satisfying. This field also keeps constantly evolving, so much that in 10 years the job requirements change drastically, and it might be difficult to find a job if we don’t keep learning.

If you enjoy learning and trying to do new stuff day by day, then this is for you. It will get easier over time, I promise!

From zero knowledge on programming to being able to apply to any job and hope to succeed, it takes at least 6 months with full day dedication (8 hours a day) if you’re a good learner.

To get rid of the “junior” title, you will need another 2-3 years usually. And (real) seniors have at least 8 years of experience. From there, the differences between someone with 8 or 20 years of experience tend to be very dim; it depends more on the people themselves than the experience.

The good news is that with enough dedication you can get yourself employed in record time, which for other careers requires proper study at a university, which takes several years. And you’ll be able to grow easily on the job. Getting employed is not the destination, but maybe a new beginning. There’s always a huge demand on good developers, so if you prove yourself good, even if the company doesn’t allow you to grow, there will be several companies wanting your talent and will raise the offer accordingly.

So, before taking this journey ask yourself: Is this the path I want to take? If the answer is yes, then do it! Commit and push forward to get it.

NOTE: I love oversimplifying a lot!. And I will lie in order to make things look simpler and create simple rules that don’t always work. This is done on purpose to make the experience easy to follow and add concepts slowly. I will correct these and get into specifics slowly as I feel the reader got enough knowledge to understand the whole thing.

Book Levels

I organized the book into sections called levels, following “mage” levels as in fantasy games.

These give a way to you to know how much progress you did and how far are from your goals.

Your target should be to reach the “Adept” level and be proficient with it. You could consider yourself a programmer just with that.

After this point, it’s a matter of wanting to be better. The better you are, the easier it will become to get a job. I would recommend at least reaching the Master level to be in a good position to start working.

Level: Starter

You know how to install Rust and create programs.

Level: Novice

Your programs know how to apply logic and process a lot of instructions.

Level: Apprentice

Your programs become useful, and you’re able to create your own utilities for your own use.

Level: Adept

Able to use most libraries out there and create programs that would be production grade. This could be enough for some starting junior positions in some companies.

Level: Master

Got rid of most of the blockers and brain-walls that are associated with Rust and programming. Coding complex stuff that matters, using the right tools for the job and being able to participate and contribute with the community.

Level: Grand Master

Expertise and in-depth knowledge on how Rust works. Able to follow almost all code that you came to see.

Level: Legendary

If you reach this level, better go and contribute to Rust itself!

Unfinished chapters

This Book keeps growing. A lot of chapters are still a work in progress.

I added some symbols on the titles to clarify:

  • ?: Pending review and completing a bit.
  • !?: Several paragraphs are still missing.
  • !!?: Still mostly empty, placeholder.

Level: Starter

Here your journey begins, little mage. This book will teach you how to get started in Rust and get your magic flowing.

Shortly you’ll be able to start casting your first spells and get your programs flowing.

The initial journey is hard, but don’t despair! We’ll go little by little.

Your first programming language

This is a common question everyone asks themselves. In my opinion, Python and Rust are the two best languages to start with. While Python will give you an easier time to begin, and plenty of jobs that actually exist, Rust is going to be a bit harder to begin with, with way less jobs.

As the title suggests, I’m going to recommend Rust. Why? Lots of reasons. I believe Rust will be the language of the future, so by the time you learn it hopefully there will be jobs waiting for you.

Rust also will teach you proper coding practices. As it is stricter, it will force you into the right shape and mindset for a programmer, and not someone who stitches things together that delivers delicate programs that break in strange ways just by looking at them.

The Rust community is very friendly. You’ll have a lot of people wanting to teach you the language and help you along on this. Python is a bit of hit-and-miss, some communities might be toxic.

The documentation is solid, nicely written, and very easy to follow. The reference docs might be a bit complex to understand at first, but after the first month or two it becomes very practical.

Rust has potential to be used for anything. Web, games, embedded devices, operating systems, browsers, you name it. There are still several gaps at the moment of this writing due to the nature of Rust being so new; over time more libraries will be created and mature, so right now there are several types of applications where Rust might not be fit. For now.

Python is very powerful and is able to do most of the applications that you might want to do, and it is very quick to write and get results with it. But it has several downsides (one is performance) that are very unlikely to disappear in the next 10 or 20 years1.

In the end, it doesn’t matter that much which language you start with. As long as you can learn to code, the language isn’t that important. All languages have lots of similarities, so much that when you learn the second one it will take 99% less time than the first.

And you can’t count on learning a single language (i.e. Rust) and hope that this would be all. There are always languages that are very good at one thing, and you should learn those too. Because you don’t want to spend a week doing something in Rust that takes 1 line of Bash to do, if there’s no real reason or benefit2 for it.

So, no worries, you’ll be fine. You’ll learn other languages over the course of years and that will take less and less time as you go.


1

As usual, there are several companies trying to make Python faster. Some efforts are going through “compiling” typed python code with great speedups. There’s also PyPy. But the community at this point does not want a “typed Python” so I don’t see the performance being fixed in the near future.

2

I’ve seen someone write a full Java application for several days, trying to make it as fast as possible, and someone else came up with a Bash one liner with pipes that did the same thing 2x faster, and they basically spent 5 minutes writing the line. Use the best tool for each job. You should learn more than one programming language.

Related books

The holy Rust book

You must know that there exists a book called just “The Rust book” or “the book”.

I will (jokingly & friendly) call it here “The holy rust book”.

You can check it here: https://doc.rust-lang.org/stable/book/

The holy book is the main learning resource for Rust. It is the true starting point for learning the language and contains everything, from beginner to truly advanced. It covers the entire language (except the uses of Unsafe, that are covered in the Necronomicon… I mean, the Rustonomicon)

This guide it is not meant to replace the holy book itself, as it is very well written by experts and reviewed by the whole community.

If that’s the case, what’s the point of this then? Well, the holy book assumes some knowledge. It will rapidly grow in difficulty and will require several reads for beginners and look in other places as well to fully grasp the concepts.

Only a small part of the language will be covered here; enough to get you started, and at a slow pace enough to follow without external resources (hopefully). From time to time I’ll link to the holy book, so you can go there to read if you like, as there’s more detail.

I call it “holy” because it’s the Rust Bible (in fact some people call it that). If the holy book disagrees with me, the book is right, I’m wrong. Whatever it says, it is right. A lot of people call Rust a cult (seriously) so I pretend to extend and embrace the term to make it a funny joke.

Other Rust books

See https://github.com/sger/RustBooks

Setting up the computer

So you reached this point! I guess we’re doing it. Let’s stop the chatter and bring in the real stuff. Let’s install Rust.

Installing Rust

Get your browser and go to https://rustup.rs/. Follow the instructions there.

  • For Windows, there’s a rust-init.exe that you download and will install everything.
  • For Linux (my case), there’s a single command line that you copy and paste into the console.

(By the way, if you want to be a developer, you also need to make the terminal your friend)

That will be all we need to do. Congrats! You installed Rust in your machine!

Chapter 1.1 - Installation of the Holy Book contains the installation instructions.

Choosing an editor for Rust

I’m very opinionated here. Get Visual Studio Code. (not to be confused with MS Visual Studio)

People call it VS Code or just “Code”. It’s open source, supports nearly all languages, and works really, really well.

From those that code with Rust, they mainly use VS Code or Vim. And I’m not going to recommend Vim to anyone. It is an excellent program, but geared towards very senior people. So VS Code it is.

I have more than 15 years coding, and I do use VS Code. It’s great. On the other hand, I almost never use Vim: it requires a lot of investment that I don’t want to commit to.

Now, go to the extensions panel and search for “rust-analyzer” and install it. This is all you need to get the best Rust experience.

VSCode Extensions

It is really important to have Rust properly installed at this point, or rust-analyzer will fail.

Your first program

Before doing anything, it is important to have a folder where you put all your programs. So go ahead and create a folder with the name you like (i.e., “programs”)1.

A few tips though:

  • Prefer to use lowercase only for the folder, no spaces. Use an underscore if you need to separate words, but I would recommend writing something short.

  • Place the folder somewhere that has a short, easy path. Don’t place it on your Desktop. On Windows, “C:" might be better as the home folder contains a path with spaces.

Now open a terminal and go to this folder. Using cd .. and cd your_folder_name should do the trick. If you followed my tips, this should be an easy task.

Once you’re set, run the following command:

$ cargo new learnrust
     Created binary (application) `learnrust` package

This will create a folder called learnrust, you should be able to find it with your file explorer. Inside, there are a few folders and files. This is how an empty Rust program looks.

It already includes a program in src called main.rs (we’ll check out the contents later). And it can be executed.

Do cd learnrust on the console to get into the folder, then run:

$ cargo run

The output will be similar to this:

$ cargo run
Compiling learnrust v0.1.0 (/home/deavid/git/rust/learnrust)
Finished dev [unoptimized + debuginfo] target(s) in 0.34s
  Running `target/debug/learnrust`
Hello, world!

The program has been built and executed. The program output is “Hello, world!”.

Congrats! You just wrote your first program. (more or less, hah)

If you execute “cargo run” again, then:

    $ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/learnrust`
    Hello, world!

Notice how something is different. At first, it compiled the program, then executed it. On the second time, it noticed that the program was unchanged, so it was run without compiling it again.

This is covered in Chapter 1.2 - Hello World! of the holy book.

What is this cargo command?

Surely you noticed that we did “cargo run” and not “rust run”. Cargo is like the swiss-army knife of Rust, it will simplify all our processes during coding, and removes a lot of stuff that we don’t need to learn.

Main things that it does:

  • Instead of compiling the program with rustc, it’s just cargo build. rustc will need flags, and it’s a bit tricky to do right, cargo makes this super simple.
  • Cargo built the program in ./target/debug/ or ./target/release. We could run these directly, but we would need to remember to build them first. cargo run does this for us.
  • We might need to download libraries for additional stuff (this is common in all programming languages). Instead of doing this manually, we can declare in Cargo.toml which ones do we want, and when we do cargo build it will download anything missing automatically.

There’s much more than this, but for now this is what we will be using. The bottom line is that we will always use cargo and forget about the other commands. That’s fewer things to remember.

As a side note, if you installed with rustup.rs as I recommended, there’s also a rustup command. This is used to update rust and cargo themselves. If you need to update, just run rustup update and everything will be done automatically. It’s a good idea to do this once a month or so, but if you don’t do it is also fine.

1

Personally I use Git for everything, and I have the folders structured as /home/deavid/git/rust/project_name.

Quick, to the IDE!

Let’s start using that nice VS Code that we installed earlier.

Open VS Code, look for the Menu File and click Open Folder....

Open Folder

Select the learnrust folder that cargo created.

NOTE: For myself, I find more convenient to open VSCode from the terminal.
I simply cd learnrust and run code .. VSCode will open the folder.

Now, open the src folder using the left panel, and you’ll see the main.rs. Double-click on it.

This is what you should see:

fn main() {
   println!("Hello, world!");
}

fn main() represents the main program.

The brackets next to it ({ and }) define where the program starts and ends.

Right now there’s only one line in the program: println!(...)

println!(...) is for printing text on the console/terminal.

The text between the parentheses is what will be printed. Notice that it’s surrounded by double quotes and these are not printed to the terminal. They’re required.

This line ends with a semicolon ; this is what marks the end of the instruction (the command to run). Rust does not care about the different lines, or how they look in your editor. You can put all in one line and still will do the same thing. VSCode will format by default when you save, so it will always look nice and tidy.

Open a new terminal inside VS Code. Go to TerminalNew Terminal:

New Terminal

This opens a terminal on the project folder, so you can now type there cargo run to run the program inside this terminal panel.

As you can see we can do the same things as with an external terminal. As this is more convenient, we’ll use this from now on. There’s no difference to an external one, so if you prefer to have a separate terminal program running, it’s totally fine.

You can try to print different texts or more lines. For example:

Program run

This is not very useful, but it’s important to play around and get familiar with what we learn.

Programs are executed one line at a time. Rust will read the first line, execute the command, then go to the next line and do the same. Until it reaches the end of the program and then the program just ends.

Saving our progress

When reading this book, you’ll find lots of small recipes to try out. You can put them in your main.rs file and execute cargo run, but soon you’ll find that you need to delete your old code to put the new one.

And you might not want to keep removing the old code. That’s understandable!

I would prefer to have something for you to build incrementally, but sadly at this point it’s not possible. We need to learn the basics for a while before I can give you any sort of tasks. So for the next chapters, we’ll be using small programs that are easy to understand. A few lines only each time.

But you, the reader, might want to keep those samples to play around later. And avoid deleting them when trying out something new.

I have a solution for you, but you’ll have to trust me here.

Multiple binaries with Cargo

First, create the folder learnrust/src/bin/, and add a file named sample1.rs in it.

In this file learnrust/src/bin/sample1.rs, add the following contents:

fn main() {
    println!("sample program 1");
}

Now open Cargo.toml file, and add the following lines:

[[bin]]
name = "sample1"

Your file now should look like this:

[package]
name = "learnrust"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at ...

[[bin]]
name = "sample1"

[dependencies]

With this done, we now have two programs in one project folder learnrust/.

Executing the new program

The new program we created is called “sample1”, and since we have two programs, we now need to specify to cargo run which program to run.

If we try to execute cargo run as usual, it will fail with this:

$ cargo run 
error: `cargo run` could not determine which binary to run. 
Use the `--bin` option to specify a binary, or the `default-run` manifest key.
available binaries: learnrust, sample1

Please read the error messages. Carefully. 99.9% of the time we get stuck because we don’t pay attention to what the error is telling us.

To fix this, we run instead cargo run --bin sample1:

$ cargo run --bin sample1
   Compiling learnrust v0.1.0 (/home/deavid/git/rust/lprfl/learnrust)
    Finished dev [unoptimized + debuginfo] target(s) in 0.19s
     Running `target/debug/sample1`
sample program 1

Now we executed the new program.

Each time we want to add a new program, we just create another file in the bin/ folder, add it to Cargo.toml as we just did, and voilà! We can have as many programs as we want.

You can name your program as you want, but needs to be all letters, lowercase and must end with .rs. It may contain numbers, but it must not begin with a number. For example example1.rs is fine, but 1example.rs is not. You can use underscores to separate words as well: my_program.rs. Don’t put spaces.

Executing the old program

Now, as you noticed, the old program in main.rs can no longer be executed by running cargo run.

Instead, we will need to run cargo run --bin learnrust:

$ cargo run --bin learnrust
   Compiling learnrust v0.1.0 (/home/deavid/git/rust/lprfl/learnrust)
    Finished dev [unoptimized + debuginfo] target(s) in 0.18s
     Running `target/debug/learnrust`
Hello, world!

Level: Novice

You got your first program working! Sorcery!

Now we’ll teach you the arcane stuff that nobody knows, the dark arts of coding.

Get ready because we’ll learn lots of new spells: if, for and much more!

Your magic will get powerful very shortly.

It’s like cooking recipes, seriously

You may not be into cooking, and that’s fine. But probably you know what a cooking recipe is: nothing more than a set of steps (instructions) that if followed produce the desired result: delicious food.

A program works in the same way. It has a set of instructions that should be followed step by step to produce the desired result.

There are stupid simple cooking recipes, for example to prepare frozen pizza:

  • Preheat the oven to 180ºC for 10 minutes
  • Remove the pizza from the box and remove the film
  • Put the pizza into the oven, on top of a tray.
  • Wait 15 minutes.
  • Turn off the oven, remove the pizza and serve.

And there are simple computer programs as well:

  • Print one line on the console that says Hello world!
  • end the program.

But a common problem in cooking recipes is that they prepare a specific amount of food. If you want more or less, you have to tweak “the program” to roughly make more or less food to meet your requirements.

In programs, we have inputs (or arguments), where we can add a value and the program will take it into account for the calculation. In recipes, this is like having a number of “people to serve” and having some formula to scale up the ingredients to get the right amount of food.

We also have conditions, which work like those recipe steps that say “cook until brown”.

There are loops, which allow us to say “do this 10 times”.

There are functions, which in recipes appears when a meal is very complicated and for a particular step says “to do the base of the cake, refer to this other recipe”. It avoids repeating ourselves every time that a set of steps we can reuse across recipes.

Of course programs can do things really complicated that are quite far from recipes, but if you’re starting to learn, this comparison will serve you to get a better grasp on how this all works.

Understanding what a program is

It might appear as a rhetorical question, but what is really a program?

In some sense, a program is a set of instructions that the computer executes in order. In our case, it will read a file and execute line by line, from the top to the bottom.

But programs are more than that.

A program can be compiled or interpreted. Interpreted programs, like Python ones, are actually reading the file and executing the lines. But compiled programs like the ones Rust does, actually create something called a “binary”.

Rust will translate all our instructions into another language called “machine code”, which is the language that our computer does understand. Then it will write the file with those instructions. This file is what we call the “binary” program.

Also, programs can interact with the computer in several ways. For example, they can accept input from the user, or communicate via the console. And they can also read arguments when they’re executed.

As part of our journey to learn programming, it is critical that we understand arguments and input/output in the console.

Program Arguments

Let’s see arguments first. When you execute a program in the console, it can accept several arguments.

An argument is basically text that we can provide a program to operate.

For example, we can open a browser from the console, passing the URL to open:

$ firefox https://google.com

The URL is an argument (or parameter) that we provided to Firefox.

In general, we provide arguments like this:

$ myprogram "argument1" "argument2" "argument3"

And just for the record, they also have “return values” which you can check in Bash1:

$ echo "hello"
$ echo $? 
0

Zero indicates success. Any other number is an error.


1

Bash is for Linux and other Unix-alike operating systems. Windows can do this too, but in a different way.

Input and Output

A program in the console has by default three ways to communicate:

  • Standard Input (stdin): This is what the user types in the console.
  • Standard Output (stdout): This is where the program writes in the console.
  • Standard Error (stderr): Here the program sends error messages, usually they appear in the console too.

Very weird names for something tremendously simple:

$ my_program
What is your name:      <--- this is stdout
Waldo                   <--- this is stdin (you type this)

Hello, waldo!

What is your age:
white

>> ERROR: white is not a number! Program failure. <--- this is stderr (an error message)
$

Hope this serves to explain the basics on how programs have their inputs and outputs for the user, as this will be useful later on.

Variables are like little boxes that store things inside

Printing some texts gets boring very fast. A computer does calculations for us, that’s what makes them useful.

It is possible to do simple calculations and print them, but this doesn’t have much mystery either:


#![allow(unused)]
fn main() {
 println!("{}", (2 * (1 + 5) + 3 / 7) / 2);
}

This will print 6, because it works with integers (whole numbers). Just like a regular calculator but without decimal points.

To get a decimal value just use all numbers as decimals, even if it’s 2.0:


#![allow(unused)]
fn main() {
println!("{}", (2.0 * (1.0 + 5.0) + 3.0 / 7.0) / 2.0);
}

This prints 6.214285714285714, just like a calculator with probably more digits.

You’ll notice that Rust will error out if you mix numbers without decimals with numbers that do have decimals. We’ll go into more detail on this soon. For now, just remember that if you want decimal points, they need to be in all numbers.

Again, this gets boring very easily. We need to spice it up with… variables!

If you did math school before, you probably remember equations. For example:

\[ 1+x = 2x+5 \]

These have an unknown that is \(x\) that must be resolved for. In this case it would be \(x=-4\).

If you hate math and equations, do not worry. This is just to anchor into something you know. In coding we don’t do equations. The computer is the one doing math, not us.

So do we agree that \(x\) is “something” whose value is \(-4\), right?

Ok, hold on to that idea. That is the same for variables. What is not the same is:

  • We don’t have equations. We have instructions. Instructions do things like storing something or printing in the terminal.
  • We don’t have unknowns, and \(x\) in this example is an unknown. In programming, we have variables. An unknown is something that we don’t know (yet), while a variable is something that always has a value, and we know it.

In Rust, we would do instead:

x = -4;

Here x is the variable. It can be any name: a, j or even a word animals. Heck, even several words together: number_of_legs_in_a_dog. (If you’re a cat person, feel free to set number_of_legs_in_a_cat instead)

number_of_legs_in_a_cat = 4;

I did it for you. You can thank me later.

The equals part is an operation, it means “to store”. It actually saves the value on the right (number 4) into the name of the left.

Variables should be named in snake_case, meaning they should be all lower case, contain only English characters, and it should start by letter. Spaces are not valid1.

So now we do have a name x or number_of_legs_in_a_cat whose value is 4.

The semicolon marks the end of the instruction. This serves to tell Rust that this line is something that needs to be executed, and to avoid confusion with the next line. If we forget the semicolon, it will think that two lines are in fact one and will get confused.

If we translate this line into English it will say:

Please store the value four into the variable named number_of_legs_in_a_cat, end of instruction.

And from this point, the computer would remember that this name equals to 4, so we could print it later:


#![allow(unused)]
fn main() {
println!("{}", number_of_legs_in_a_cat);
}

This is actually the same as doing:


#![allow(unused)]
fn main() {
println!("4");
}

So, if this is the case, why do we complicate this so much?

Well, variables will help us do much more complex programs, as they can keep track of what was the user input or other data that we are managing inside. It will make sense soon, so bear with me for now.


1

Actually it’s more flexible than that, Rust also allows some emojis; but for simplicity, let’s use only English alphabet.

Exercise: Printing and Formatting

So we saw that println!() does print lines in the console. Let’s practice a bit and see other fun ways of using println!().

First, we’ll create another program which we’ll call print.rs.

Go ahead and create a new file in learnrust/src/bin/print.rs.

Add the contents:

fn main() {}

Now open Cargo.toml and add:

[[bin]]
name = "print"

Your Cargo.toml should now look like this:

[package]
name = "learnrust"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at ...

[[bin]]
name = "sample1"

[[bin]]
name = "print"

[dependencies]

We can now run this program with cargo run --bin print:

$ cargo run --bin print
   Compiling learnrust v0.1.0 (/home/deavid/git/rust/lprfl/learnrust)
    Finished dev [unoptimized + debuginfo] target(s) in 0.66s
     Running `target/debug/print`

As you can see, aside of compiling the program, this does nothing. Pretty obvious when you see the code, right? It’s almost empty.

Printing banners of text

Say we want to have some banner when the program starts. That would require multiple lines and will describe what is this program.

While it’s possible to have multiple lines in a single println!(), I don’t recommend this. It’s hard to read and hard to write.

Instead, we’ll just use one println!() per line:


#![allow(unused)]
fn main() {
    println!();
    println!("#############################################################");
    println!("#                                                           #");
    println!("#                This is a PRINT program                    #");
    println!("#                                                           #");
    println!("#############################################################");
    println!();
    println!("Summary: This program demonstrates different");
    println!("         ways of printing text");
    println!();
}

HINT: On the piece of code above there should appear a “play” button if your browser supports JavaScript. If you click it, you can see the output of this program. The play button appears in most of the pieces of code in this book, so you don’t need to try every single program out.

Placeholders

Now, printing text is nice. But soon we will want to replace a part of the text with something we can replace later on.

For example, consider this piece of code:


#![allow(unused)]
fn main() {
    println!("The sum of 2 + 3 is {}. Isn't that great?", 2 + 3);
}

The placeholder is {}. These two characters tell Rust that “here we want to put a value”. Then after the text, we add a comma and write down the value we want. It can be a number, text, or like in this case, an operation.

Rut basically will do the following steps:

  1. It will compute the sum we wrote:

#![allow(unused)]
fn main() {
    println!("The sum of 2 + 3 is {}. Isn't that great?", 5);
}
  1. Replaces the placeholder with the value:

#![allow(unused)]
fn main() {
    println!("The sum of 2 + 3 is 5. Isn't that great?");
}
  1. Prints the text into the console:
The sum of 2 + 3 is 5. Isn't that great?

We can use more than one placeholder, just add more values at the end separated by commas:


#![allow(unused)]
fn main() {
    println!("And {} * {} is {}. Fantastic.", 4, 3, 12);
}

This prints: And 4 * 3 is 12. Fantastic.

The values must appear in the same order as the placeholders. The first value corresponds to the first {} from the left, the second value to the second {}. You get the idea.

If it’s empty, without text or values, it will just print a new line. This can be useful to separate parts of the output:


#![allow(unused)]
fn main() {
    println!();
}

But be careful! We cannot put values without a placeholder. This DOES NOT WORK:


#![allow(unused)]
fn main() {
println!(5 * 10);
}

HINT: Try to click the play button for the above code. It should show you the error that Rust gives for this particular code.

Instead, if we just want a value, we need to add a placeholder for it:


#![allow(unused)]
fn main() {
println!("{}", 5 * 10);
}

And of course, the number of placeholders must be equal to the number of values we added. If too many placeholders appear, it will error out:


#![allow(unused)]
fn main() {
println!("{} this placeholder does not have a value->{}", 5 * 10);
}

And in the reverse, if we have too many values, it is also an error:


#![allow(unused)]
fn main() {
println!("{}", 5, 10, 5 * 10);
}

Understanding Rust errors

Again, please read the errors carefully. They explain a lot of what is wrong.

For example, on the above code we had:

error: 2 positional arguments in format string, but there is 1 argument
 --> src/main.rs:4:11
  |
4 | println!("{} this placeholder does not have a value->{}", 5 * 10);
  |           ^^                                         ^^   ------

error: could not compile `playground` due to previous error

ALWAYS read the errors from top to bottom! Start from the first line and keep reading line by line, like a book. I know this sounds obvious, but in the console, we tend to read just the last line:

error: could not compile `playground` due to previous error

In fact, most people just reads:

error: could not compile

And forgets about anything else. I swear. Some people just reads error and gets puzzled. Don’t be like them. Read the errors from top to bottom.

Let’s break down the error in small parts to understand what it means:

(...) 2 positional arguments in format string (...)

By “positional arguments”, Rust means that we wrote two placeholders. And “format string” is the text to print "{} this placeholder does not have a value->{}".

(...), but there is 1 argument

And the “argument” is the value on the right of the text, in our case , 5 * 10.

So, let’s read again:

error: 2 positional arguments in format string, but there is 1 argument

Therefore, what Rust means here is:

You wrote two placeholders {} in the text, but you only provided one value on the right: , 5 * 10

And if we keep reading (we must keep reading!), we have a nice help of what’s happening:

 --> src/main.rs:4:11
  |
4 | println!("{} this placeholder does not have a value->{}", 5 * 10);
  |           ^^                                         ^^   ------

Notice the first line: --> src/main.rs:4:11. This tells us which file has the problem (in my case I executed this in the browser, so the file does not match with what we have in our project). The :4:11 means line 4, character 11. Rust is telling us exactly (down to which character) has the problem. This is super useful!!

In the next lines, see how it is showing the line of the program that is affected. And the characters bellow (^^ and -----) are underlining the parts that are contributing to this error.

Error messages are super helpful! Keep reading them1. They will explain to you how to fix your program.

1

Each time that you reach out for help for an error and turns out that you didn’t read the error message carefully, put one coin into your “Haven’t read the error message” jar. Use the contents to invite your helper friends to a drink from time to time.

Formatting numbers

Now that I’ve been a pain in the neck for long enough about errors, let’s do something interesting again. Sorry about that, but you’ll thank me later.

Let’s say we want to print some decimal numbers. But sometimes, they’re ugly:


#![allow(unused)]
fn main() {
println!("5.0 / 3.0 = {}", 5.0 / 3.0);
}

This prints 5.0 / 3.0 = 1.66666666666666672, and it might be difficult to read. Wouldn’t it be nice if we could round to two decimals?

Let’s see a few samples:


#![allow(unused)]
fn main() {
    println!("5.0 / 3.0 = {}", 5.0 / 3.0);
    println!("5.0 / 3.0 = {:.3}", 5.0 / 3.0);
    println!("5.0 / 3.0 = {:.2}", 5.0 / 3.0);
    println!("5.0 / 3.0 = {:.0}", 5.0 / 3.0);
}

Click on the play button and see how they are rounded to different decimal places. Also, notice that Rust rounded the number, so 1.6 becomes 2.

You can read the full documentation on formatting in here: https://doc.rust-lang.org/std/fmt/

We can also do leading zeros:


#![allow(unused)]
fn main() {
println!("10 / 2 = {:04}", 5);
}

And we can mix both together, but be careful because the leading zeros count all digits and dot. So you need more “leading zeros” to cover for the decimal places:


#![allow(unused)]
fn main() {
println!("5.0 / 3.0 = {:07.2}", 5.0 / 3.0);
}
2

Note that, depending on your computer and Rust version you might see more or less decimals; Also the last digits might change. This is normal. Don’t think much about it. The explanation is an advanced concept that I hope to get at later stages of this book. For now, let’s move on.

Done!

That’s it for now! Feel free to play around and try yourself.

Here’s the program completed:

fn main() {
    println!();
    println!("#############################################################");
    println!("#                                                           #");
    println!("#                This is a PRINT program                    #");
    println!("#                                                           #");
    println!("#############################################################");
    println!();
    println!("Summary: This program demonstrates different");
    println!("         ways of printing text");
    println!();
    println!("The sum of 2 + 3 is {}. Isn't that great?", 2 + 3);
    println!("And {} * {} is {}. Fantastic.", 4, 3, 12);
    println!();
    println!("5.0 / 3.0 = {}", 5.0 / 3.0);
    println!("5.0 / 3.0 = {:.3}", 5.0 / 3.0);
    println!("5.0 / 3.0 = {:.2}", 5.0 / 3.0);
    println!("5.0 / 3.0 = {:.0}", 5.0 / 3.0);
    println!();
    println!("10 / 2 = {:04}", 5);
    println!("5.0 / 3.0 = {:07.2}", 5.0 / 3.0);
    println!();
}

Remember to write save it in learnrust/src/bin/print.rs and edit Cargo.toml as I explained on top.

Then run it with: cargo run --bin print

$ cargo run --bin print
   Compiling learnrust v0.1.0 (/home/deavid/git/rust/lprfl/learnrust)
    Finished dev [unoptimized + debuginfo] target(s) in 1.11s
     Running `target/debug/print`

#############################################################
#                                                           #
#                This is a PRINT program                    #
#                                                           #
#############################################################

Summary: This program demonstrates different
         ways of printing text

The sum of 2 + 3 is 5. Isn't that great?
And 4 * 3 is 12. Fantastic.

5.0 / 3.0 = 1.6666666666666667
5.0 / 3.0 = 1.667
5.0 / 3.0 = 1.67
5.0 / 3.0 = 2

10 / 2 = 0005
5.0 / 3.0 = 0001.67

Having fun with libraries

Doing everything by ourselves might be rewarding, but it is also tiring. It takes a lot of effort and knowledge to code a proper program that is really useful. But I like a saying that goes: “Don’t reinvent the wheel”

If you need something, it is highly probable that someone else did it already and shared it for free. Seriously. Most of the time it is just a problem of not googling enough, or using the right keywords.

People share their functions and modules as libraries called “crates” in a site called https://crates.io.

As a beginner, it is important to use as many crates as you can1. They will allow you to create interesting programs easily, giving you a good sense of progress.

The first library I want you to try is “rand”: https://crates.io/crates/rand

This crate as the name suggests creates random numbers. We need to install it in our project, and for that, notice that on the right side of the website there are Install instructions and Documentation:

So we will follow the installation instructions. We need to open our Cargo.toml file first. This file was created when we did cargo new.

 [package]
 name = "learnrust"
 version = "0.1.0"
 edition = "2021"
 
 # See more keys and their definitions at ...
 
 [dependencies]
 rand = "0.8.4"   # Add dependencies here

We add the new crate to this file by inserting the line rand = "0.8.4" as the instructions say just below the [dependencies].

If we execute now cargo run we get something different already:

 $ cargo run
   Compiling cfg-if v1.0.0
   Compiling ppv-lite86 v0.2.15
   Compiling libc v0.2.107
   Compiling getrandom v0.2.3
   Compiling rand_core v0.6.3
   Compiling rand_chacha v0.3.1
   Compiling rand v0.8.4
   Compiling learnrust v0.1.0 (/home/deavid/git/rust/learnrust)
    Finished dev [unoptimized + debuginfo] target(s) in 2.54s
     Running `target/debug/learnrust`
 Hello world

Cargo noticed that we have added a new dependency, so it downloaded the required libraries and compiled them too! A lot of work that didn’t require almost any manual action from our side.

The program however still does the same thing. Just installing a library is not going to make our program generate random numbers.

The simplest way to start with it is to use rand::random():

fn main() {
   let randnum: i64 = rand::random();
   println!("Hello world: {}", randnum);
}

We need to define the type of randnum this time. The reason is that this library detects where we want to save it and creates a random number as big as possible that does fit.

The result is:

 deavid@debian:~/git/rust/learnrust$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/learnrust`
 Hello world: 1976599895379426978
 deavid@debian:~/git/rust/learnrust$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/learnrust`
 Hello world: -2920297650750248329
 deavid@debian:~/git/rust/learnrust$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/learnrust`
 Hello world: 8971769657686972258
 deavid@debian:~/git/rust/learnrust$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/learnrust`
 Hello world: -8688429354790802443

As we can see it creates very big numbers, both positive and negative. A different random number will be generated in each run.

We could make the number non-negative and smaller by choosing u8 instead of i64:


#![allow(unused)]
fn main() {
   let randnum: u8 = rand::random();
}

This creates the following results:

 Hello world: 140
 Hello world: 215
 Hello world: 99
 Hello world: 221

Because u8 is unsigned and can hold numbers between 0 and 255 \((2^8-1)\), this is the range that we get.

We can instead ask this library for a specific range:

use rand::Rng;

fn main() {
   let mut rng = rand::thread_rng();
   let randnum: i64 = rng.gen_range(1..=100);
   println!("Hello world: {}", randnum);
}

In this case we need to create a random number generator object rng. This will be holding the internal state of the random generator. It needs to be mutable because the state changes as each number is generated, so the numbers aren’t repeated every time.

The gen_range(1..=100) specifies which range of numbers to retrieve, like in a for loop, but in this case we only get a single number, randomly.

The use rand::Rng on the top is needed to access the gen_range method. This is what Rust calls a Trait. We will go over these later on.

1

Of course, don’t go crazy and import the whole site. It’s not a race to see who imports more crates. Also, as you gain more experience you should be able to keep the number of imported libraries low.

Declaring variables

Before we can store values into a variable, we need to declare it. Declaring a variable means to tell Rust to create it, we will be explaining to it that this name is something that we will be using later.

Some programming languages don’t require declaring, and just storing a value for the first time will do the trick. This is the case for Python, but not for Rust.

Anyway, it’s not a big deal. Declaring a variable is very easy, we just have to use let:


#![allow(unused)]
fn main() {
   let x;
}

This comes from the wording in math of “let x be a number…”, so we use the keyword let to announce new variable names.

A keyword is a reserved name by the programming language. So this means that you can’t have a variable named let, because let let; would be confusing for Rust to understand your program.

A very simple program that makes use of variables could be:

   fn main() {
      let x;
      x = 4;
      println!("{}", x);
   }

You already know what it does. Prints 4. That’s all.

A variable can change their value at any time. For this we need to use let mut x instead of let x, so Rust knows that we want to mutate the value inside this variable later on (more on this later).

An example:

   fn main() {
      let mut x;
      x = 4;
      println!("{}", x);
      x = 6;
      println!("{}", x);
      x = 1;
      println!("{}", x);
   }

That will print 4, then 6, then 1. It’s just that the variable has changed the value it contained over time, as the program runs. Then the println!() just reads the value at that point in time and prints it to the terminal.

An important thing here, we have to use let mut x instead of just let x to tell Rust that this variable is “mutable”, this means that we can change the contents later on.

We can declare one variable per line, each one with its own let:


#![allow(unused)]
fn main() {
let x;
let y;
let z;
}

But we can’t declare these in a single let instruction. Other programming languages such as C++ allow separating them with commas, but not in Rust. We need one line for each one.

Most of the time we declare a variable we actually want to give them a value, because after all a value must always have a value at all times. So we can save a few lines and do it in one shot:


#![allow(unused)]
fn main() {
let x = 4;
let y = 3;
let z = 2;
}

So now this does two things at the same time, it declares, and it stores a value. To be clear, this is just shorter and nicer on our eyes. To the computer, it is exactly the same as if we declared first, and then we used another 3 lines to store the value. The program will be identical and will run equally fast.

Bottom line: do you prefer to see it on three lines all together or in six lines? Which one is easier to understand and read for you? Whatever is your response, that should be what you should write.

We don’t write the programs for computers, we write them for humans to understand. If you think that a particular way is easier to read and understand, go with it.

As you’ll start to notice by now, there are several ways to write a program (in fact, they’re infinite). This might feel annoying. Worse even, there’s no “right way”. There are subjectively better and worse ways, but it’s always up to humans to define what looks and feels better and come up with reasons for it.

Don’t be bothered about this, don’t think much about this. It is fine. Just write what it feels better to you personally, and you’ll be grand. Over time, you’ll learn more about how to make the code more readable, but that comes with experience. For now, let’s focus on learning this.

Exercise: Variables

In the last exercise we saw lots of ways to print, but to be fair, using placeholders doesn’t make much sense if we have to write what goes inside, right?

So in this one we’ll explore how to combine the previous things we learned with variables.

First things first, we’ll create a new program again. This time we will call it variables.rs.

So go ahead and create it in the folder learnrust/src/bin/.

As usual, we will start with fn main() {}:

fn main() {

}

Now go to Cargo.toml and add:

[[bin]]
name = "variables"

Test that it works by running:

$ cargo run --bin variables

Hello, Waldo!

Let’s try a variation of the mythical “Hello, World!” program:


#![allow(unused)]
fn main() {
    let name = "Waldo";
    println!("Hello, {}!", name);
}

And we can try a few variables with numbers and math:


#![allow(unused)]
fn main() {
    let a = 3;
    let b = 7;
    let c = a * b;
    println!("The result of {} * {} is {}", a, b, c);
}

Let’s try to mutate one variable over and over:


#![allow(unused)]
fn main() {
    let mut x = 4;
    let a = 3;
    println!("{}", x);
    x = 6;
    println!("{}", x);
    x = 1 + a;
    println!("{}", x);
}

We can also do operations with variables:


#![allow(unused)]
fn main() {
    let x = 4;
    let a = 3;
    println!("{} + {} = {}", x, a, x + a);
}

HINT: Remember you have the play button/icon on each code block to execute these samples in your browser.

Done!

Here’s the full program:

fn main() {
    let name = "Waldo";
    println!("Hello, {}!", name);

    let a = 3;
    let b = 7;
    let c = a * b;
    println!("The result of {} * {} is {}", a, b, c);

    let mut x = 4;
    let a = 3;
    println!("{}", x);
    x = 6;
    println!("{}", x);
    x = 1 + a;
    println!("{}", x);

    let x = 4;
    let a = 3;
    println!("{} + {} = {}", x, a, x + a);
}

The output is:

Hello, Waldo!
The result of 3 * 7 is 21
4
6
4
4 + 3 = 7

Let’s try Turtle!

For learning purposes, there was a little program called turtle in the 80s for a programming language called LOGO.

This was intended for kids to learn coding. It displayed a little turtle, and they could send commands such as:

  • Move right 30 pixels.
  • Move top 30 pixels.
  • Move left 30 pixels.
  • Move bottom 30 pixels.

And that would create a rectangle on screen.

This is fascinating for learning because there is a visual feedback on the screen. And let’s face it, the console is booooring.

In Rust, there’s a library for this and I think it’s worth exploring.

If it doesn’t work for you, or if you prefer console, just move on. I’ll have examples for everything in the console.

The library is this one: https://crates.io/crates/turtle

What we should do now is add this library as a dependency to our Cargo.toml like this:

[dependencies]
turtle = { git = "https://github.com/sunjay/turtle.git", tag = "v1.0.0-rc.2" }

We just need to add the line1 turtle = { git = "https://github.com/sunjay/turtle.git", tag = "v1.0.0-rc.2" } at the end of the file, just below [dependencies].

BEFORE GOING FURTHER: Please verify this does work for you. Run cargo build on the terminal and ensure that there are no errors.

Here’s what happens on my computer after running this the first time:

$ cargo build
   Compiling lazy_static v1.4.0
   Compiling bitflags v1.3.2
   Compiling byteorder v1.4.3
   Compiling scopeguard v1.1.0
   Compiling cfg-if v0.1.10
   Compiling piston-float v1.0.1
   Compiling rand_core v0.4.2
   Compiling void v1.0.2
   Compiling adler32 v1.2.0
   Compiling downcast-rs v1.2.0
   Compiling piston-graphics_api_version v0.2.0
   Compiling same-file v1.0.6
   Compiling lzw v0.10.0
   Compiling either v1.6.1
   Compiling cty v0.2.2
   Compiling color_quant v1.1.0
   Compiling piston-texture v0.8.0
   Compiling percent-encoding v2.1.0
   Compiling scoped_threadpool v0.1.9
   Compiling read_color v1.0.0
   Compiling interpolation v0.2.0
   Compiling fnv v1.0.7
   Compiling piston-shaders_graphics2d v0.3.1
   Compiling itoa v1.0.1
   Compiling ryu v1.0.9
   Compiling svg v0.6.0
   Compiling libloading v0.6.7
   Compiling lock_api v0.3.4
   Compiling draw_state v0.8.0
   Compiling inflate v0.4.5
   Compiling piston-viewport v1.0.2
   Compiling vecmath v1.0.0
   Compiling rand_core v0.3.1
   Compiling rand_jitter v0.1.4
   Compiling raw-window-handle v0.4.3
   Compiling libc v0.2.125
   Compiling crossbeam-utils v0.8.8
   Compiling maybe-uninit v2.0.0
   Compiling crc32fast v1.3.2
   Compiling shader_version v0.6.0
   Compiling walkdir v2.3.2
   Compiling gif v0.10.3
   Compiling stb_truetype v0.3.1
   Compiling deflate v0.7.20
   Compiling num-traits v0.2.15
   Compiling memoffset v0.6.5
   Compiling libloading v0.5.2
   Compiling dlib v0.4.2
   Compiling rand_hc v0.1.0
   Compiling rand_xorshift v0.1.1
   Compiling rand_isaac v0.1.1
   Compiling crossbeam-channel v0.5.4
   Compiling rand_chacha v0.1.1
   Compiling rand_pcg v0.1.2
   Compiling gfx_core v0.9.2
   Compiling smallvec v0.6.14
   Compiling crossbeam-epoch v0.9.8
   Compiling wayland-sys v0.21.13
   Compiling glutin_egl_sys v0.1.5
   Compiling gfx_gl v0.6.1
   Compiling serde v1.0.137
   Compiling gl v0.11.0
   Compiling dirs-sys v0.3.7
   Compiling nix v0.14.1
   Compiling num_cpus v1.13.1
   Compiling memmap v0.7.0
   Compiling x11-dl v2.19.1
   Compiling raw-window-handle v0.3.4
   Compiling shared_library v0.1.9
   Compiling rand_os v0.1.3
   Compiling parking_lot_core v0.6.2
   Compiling png v0.15.3
   Compiling crossbeam-deque v0.8.1
   Compiling dirs v4.0.0
   Compiling rand v0.6.5
   Compiling osmesa-sys v0.1.2
   Compiling ordered-float v1.1.1
   Compiling approx v0.3.2
   Compiling line_drawing v0.7.0
   Compiling num-integer v0.1.45
   Compiling tiff v0.3.1
   Compiling parking_lot v0.9.0
   Compiling rayon-core v1.9.3
   Compiling xdg v2.4.1
   Compiling rusttype v0.8.3
   Compiling num-rational v0.2.4
   Compiling num-iter v0.1.43
   Compiling gfx v0.18.2
   Compiling gfx_device_gl v0.16.2
   Compiling rusttype v0.7.9
   Compiling andrew v0.2.1
   Compiling piston2d-graphics v0.35.0
   Compiling rayon v1.5.3
   Compiling glutin_glx_sys v0.1.7
   Compiling pistoncore-input v0.28.1
   Compiling serde_json v1.0.81
   Compiling wayland-commons v0.21.13
   Compiling wayland-client v0.21.13
   Compiling jpeg-decoder v0.1.22
   Compiling pistoncore-window v0.44.0
   Compiling pistoncore-event_loop v0.49.0
   Compiling wayland-protocols v0.21.13
   Compiling piston v0.49.0
   Compiling image v0.22.5
   Compiling smithay-client-toolkit v0.4.6
   Compiling piston-gfx_texture v0.40.0
   Compiling piston2d-gfx_graphics v0.66.0
   Compiling winit v0.19.5
   Compiling glutin v0.21.2
   Compiling pistoncore-glutin_window v0.63.0
   Compiling piston_window v0.105.0
   Compiling turtle v1.0.0-rc.3
   Compiling learnrust v0.1.0 (/home/deavid/git/rust/lprfl/learnrust)
    Finished dev [unoptimized + debuginfo] target(s) in 14.61s

If we run it again, it will be much shorter. This is correct:

$ cargo build
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
1

I had to use the Git repository and the tag v1.0.0-rc.2 because v1.0.0-rc.3 which is the latest one crashes for me, and just specifying the RC version doesn’t seem to be something that Cargo likes for this package.

Testing Turtle

Let’s begin by creating a file turtle_vars.rs in learnrust/src/bin/ and place the following example code inside:

use turtle::Turtle;

fn main() {
    let mut turtle = Turtle::new();

    turtle.forward(10.0);
}

Now, remember that we have to add this program into Cargo.toml:

[[bin]]
name = "turtle_vars"

And now, let’s test this:

$ cargo run --bin turtle_vars

This should display a window like this:

Small arrow up

If this is what you see, congrats! It works for you!

Drawing a rectangle

As explained at the beginning, we just need a few commands to do this.

Basically, we can tell the turtle to turn right while moving, and if done in four steps, we should get a rectangle:


#![allow(unused)]
fn main() {
    turtle.forward(100.0);
    turtle.right(90.0);
    turtle.forward(100.0);
    turtle.right(90.0);
    turtle.forward(100.0);
    turtle.right(90.0);
    turtle.forward(100.0);
}

And this gives us a nice animation. The end result is:

Square

But wait! What does 100.0 mean? And 90.0?

The first one is the distance to move, the second one is the angle to turn.

So the program really says:

  • Walk forward 100 pixels.
  • Turn right 90 degrees.
  • Now walk forward again 100 pixels.
  • Turn right again 90 degrees.
  • Walk again 100 pixels.
  • Right by 90 degrees.
  • Walk 100 pixels.

But to someone reading the program it might be confusing to understand.

Also, what if we want to make the rectangle bigger or smaller? We would need to change the 100.0 in all four places. That’s a lot of work!2

So let’s use variables to prevent repetition:


#![allow(unused)]
fn main() {
    let distance = 100.0;
    let angle = 90.0;

    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
}

Nice! Now if we want to make it bigger we just change:


#![allow(unused)]
fn main() {
    let distance = 200.0;
    let angle = 90.0;
}

But what happens if we change the angle? Let’s try it out!


#![allow(unused)]
fn main() {
    let distance = 200.0;
    let angle = 120.0;
}

Triangle

It draws a triangle! Wait, what?!

That’s because a regular polygon has their sides separated by an angle defined by:

\[angle = 360 / sides \]

And \(360/3=120\), so 120 degrees is just a triangle.

Wait a second… can we make this, so it can do different polygons?

Sure! First we copy the forward and right commands a few times more:


#![allow(unused)]
fn main() {
    let distance = 200.0;
    let angle = 120.0;

    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
}

Now we can compute the angle by using the formula:


#![allow(unused)]
fn main() {
    let sides = 5.0;
    let distance = 200.0;
    let angle = 360.0 / sides;
}

And we get a new figure:

Pentagon

Try playing yourself with the values and see what happens each time.

  • What happens if we put sides = 10.0 and distance = 50.0? Why?
  • What happens if we change angle = 300.0 and sides = 8.0?
  • And angle = 315.0 and sides = 5.0?

2

Yes, we programmers are lazy. How could you have guessed that?

That’s it!

Well done. Hope you enjoyed this.

Yes, there’s still a lot of repetition in the code, but well get to that very soon.

Here’s the full program:

use turtle::Turtle;

fn main() {
    let mut turtle = Turtle::new();

    let sides = 5.0;
    let distance = 200.0;
    let angle = 360.0 / sides;

    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
}

Incrementing and decrementing

There’s a lot of stuff we can do with variables, but a very common thing is to use them to count, so an instruction that just says “add one to X” is quite handy:

 fn main() {
   let mut x = 4;
   println!("{}", x);
   x += 1;
   println!("{}", x);
   x += 2;
   println!("{}", x);
   x += 3;
   println!("{}", x);
  }

This program will output 4, 5, 7 and 10.

The other way around, subtracting, is also possible:

 fn main() {
   let mut x = 10;
   println!("{}", x);
   x -= 1;
   println!("{}", x);
   x -= 1;
   println!("{}", x);
   x -= 1;
   println!("{}", x);
  }

This one returns 10, 9, 8 and 7.

These operations are just a shorthand of this:


#![allow(unused)]
fn main() {
   x = x + 1;
   x = x - 1;
}

Which just means: read “x”, add one, and write the result into “x” again; overwriting the previous content.

A reminder: these are instructions, not math equations. The equals sign stores on the left the result of evaluating the right side.

Most operations you can think of that take the same form have a shortened operator as well. For example doubling a number is just x *= 2, which means x = x * 2.

The modulo or remainder operator

Something that is little known is that in programming we have an operator to get the remainder of a division.

For example, if we do 12 / 10 = 1, but the remainder of that division is 2.

To get the remainder we use the modulo operator %:


#![allow(unused)]
fn main() {
let v = 35;
let b = 10;
let remainder = v % b;
println!("{} % {} = {}", v, b, remainder);
}

This operator is useful when we want a value that wraps around a particular number. For example when counting time, seconds goes to 60, then back to zero.

So if we do:


#![allow(unused)]
fn main() {
let time_sec = 100322;
let seconds = time_sec % 60;
}

It will make the seconds variable to be on the range 0…60, excluding 60.

This, with the use of divisions, it can make for a very easy code to transform a lot of seconds into hours, minutes, and seconds. Here’s the recipe.


#![allow(unused)]
fn main() {
let time_sec = 100322;
let seconds = (time_sec) % 60;
let minutes = (time_sec / 60) % 60;
let hours =   (time_sec / 60 / 60) % 24;
let days =    (time_sec / 24 / 60 / 60) % 30;
let months =  (time_sec / 30 / 24 / 60 / 60) % 12;
let years =   (time_sec / 12 / 30 / 24 / 60 / 60);
}

NOTE: The operator actually computes the modulo, not the remainder, which is almost the same, but not identical. For negative values it behaves differently.

Looping’ around

I’m sure you feel that variables don’t do that much. But that’s because the programs we can write up to now are too linear and simple. We need to step up the game with… loops!

Loops are ways of repeating the same piece of code several times without need of copying and pasting. For example, imagine we want to make a simple program that counts from 1 to 100:

fn main() {
   println!("Number {}", 1);
   println!("Number {}", 2);
   println!("Number {}", 3);
   // ...
   println!("Number {}", 98);
   println!("Number {}", 99);
   println!("Number {}", 100);
 }

As you can imagine, this gets tedious very easily. Copying, pasting and changing all the numbers manually is cumbersome.

Presenting… the for loop!

fn main() {
   for number in 1..=100 {
       println!("Number {}", number);
   }
 }

This program does exactly the same thing in just three lines! Amazing, isn’t it? Now variables are actually being useful.

The syntax for this is as follows1:


#![allow(unused)]
fn main() {
for variable_name in first_number..=last_number
}

We are asking the program to have a variable that counts from 1 to 100. The current count number will be stored in the variable number (which we can name it as we like).

The ..= in between the numbers defines a range. The equals on the right means that it includes the right number. There also exists .. which does not include the last number (i.e., 0..100 counts from 0 to 99).

We can also put a loop inside a loop, so we can count in two directions. This could be useful to describe all positions in a chess board:

fn main() {
   for row in 1..=8 {
       for column in 1..=8 {
           println!("Row {}, Column {}", row, column);
       }
   }
 }

And of course, there’s no limit. You could put three or ten loops one inside another. The limit is your imagination here!

In this code, it will first pick a row, then go over all the columns. When the program finishes all 8 columns, it will proceed with the next row.


1

As usual, I’m lying, and it’s not the real syntax. For loops are way more powerful than this; we’ll get to that later.

Exercise: loops

Do you know Fibonacci numbers? These are a sequence in math that go:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144,...

Each number in this list has to be the sum of the previous two numbers.

We start with zero and one and begin adding them together:

0 + 1 = 1
1 + 1 = 2
1 + 2 = 3
2 + 3 = 5
3 + 5 = 8

But why Fibonacci numbers? Well, they’re easy to calculate. Nothing too fancy.

Also, they are closely related to lots of stuff in math. The golden ratio. The pascal triangle. The rate at which vampires (and bunnies) breed. The size of an A4. The binomial distribution.

https://en.wikipedia.org/wiki/Fibonacci_number

Let’s make a program that calculates several thousands of these!

Yeah, exciting. I know.

Again, let’s make a new binary fibonacci.rs inside src/bin/ and add a fn main() {}.

Also, in Cargo.toml, remember to add:

[[bin]]
name = "fibonacci"

At some point, I’ll stop telling you to do these steps. Don’t forget to do this!

Let’s create the main thingy

So we know we want lots of numbers and a loop.

First, let’s do it without a loop for 2 or 3 numbers.


#![allow(unused)]
fn main() {
    let n1 = 0;
    let n2 = 1;
    println!("{} {}", n1, n2);

    let n3 = n1 + n2;

    println!("{}", n3);

    let n1 = n2;
    let n2 = n3;

    let n3 = n1 + n2;

    println!("{}", n3);
}

We first define n1 and n2 as the initial numbers \(0,1\). This part is what initializes the Fibonacci.

Then we compute n3 = n1 + n2. Each number is the sum of the last two.

In the next step we need to move the numbers to the left, so n3 becomes n2 and n2 becomes n1. And then we can compute n3 again.

However, the order here matters. If we first move n3 into n2, we will lose the contents in n2.

Instead, we need to move first n2 to n1 to avoid losing any data.

Now onto the loop!

We make the n1 and n2 variables mutable and keep everything in a loop:


#![allow(unused)]
fn main() {
    let mut n1 = 0;
    let mut n2 = 1;
    println!("{} {}", n1, n2);
    for _ in 0..10 {
        let n3 = n1 + n2;

        println!("{}", n3);

        n1 = n2;
        n2 = n3;
    }
}

That’s it! We have Fibonacci numbers!

It’s really fast, right?

NOTE: We had to use the mut keyword to indicate to Rust that we want to be able to change the contents of a variable. Otherwise, the compiler would complain.

But it’s hard to read. Let’s do something about it. What if we print 10 per line?


#![allow(unused)]
fn main() {
    let mut n1 = 0;
    let mut n2 = 1;
    print!("{},{},", n1, n2);
    for _ in 0..10 {
        for _ in 0..10 {
            let n3 = n1 + n2;

            print!("{},", n3);

            n1 = n2;
            n2 = n3;
        }
        println!()
    }
}

Oh no…

0,1,1,2,3,5,8,13,21,34,55,89,
144,233,377,610,987,1597,2584,4181,6765,10946,
17711,28657,46368,75025,121393,196418,317811,514229,832040,1346269,
2178309,3524578,5702887,9227465,14930352,24157817,39088169,63245986,102334155,165580141,
thread 'main' panicked at 'attempt to add with overflow', src/bin/fibonacci.rs:45:22
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

What happened here?

Turns out that our numbers are getting too big, and they don’t fit.

By default, Rust is using 32bits (4 bytes) for each number, so they can hold any number up to \(2^{31} = 2147483648\).

We could tell Rust to use 64bit instead, but no. We’ll be using floating points.

Floating points have less precision than integers, but they can cover numbers way bigger and way smaller than integers themselves.

All that we need to do is add a decimal place to n1 and n2:


#![allow(unused)]
fn main() {
    let mut n1 = 0.0;
    let mut n2 = 1.0;
    print!("{},{},", n1, n2);
    for _ in 0..10 {
        for _ in 0..10 {
            let n3 = n1 + n2;

            print!("{},", n3);

            n1 = n2;
            n2 = n3;
        }
        println!()
    }
}

Yay! That worked!

0,1,1,2,3,5,8,13,21,34,55,89,
144,233,377,610,987,1597,2584,4181,6765,10946,
17711,28657,46368,75025,121393,196418,317811,514229,832040,1346269,
2178309,3524578,5702887,9227465,14930352,24157817,39088169,63245986,102334155,165580141,
267914296,433494437,701408733,1134903170,1836311903,2971215073,4807526976,7778742049,12586269025,20365011074,
32951280099,53316291173,86267571272,139583862445,225851433717,365435296162,591286729879,956722026041,1548008755920,2504730781961,
4052739537881,6557470319842,10610209857723,17167680177565,27777890035288,44945570212853,72723460248141,117669030460994,190392490709135,308061521170129,
498454011879264,806515533049393,1304969544928657,2111485077978050,3416454622906707,5527939700884757,8944394323791464,14472334024676220,23416728348467684,37889062373143900,
61305790721611580,99194853094755490,160500643816367070,259695496911122560,420196140727489660,679891637638612200,1100087778366101900,1779979416004714000,2880067194370816000,4660046610375530000,
7540113804746346000,12200160415121877000,19740274219868226000,31940434634990100000,51680708854858330000,83621143489848430000,135301852344706760000,218922995834555200000,354224848179262000000,573147844013817200000,

Okay, I know. This is not really helpful. This program might be somewhat helpful to some mathematician, but not for you and not for me either.

But we have to endure these silly examples for now. At our current level it is very hard to find anything to do that it’s actually possible with what I explained so far.

It is important to practice. And these examples will help you get an idea on how the syntax works.

Turtle: loops

We had this program:

use turtle::Turtle;

fn main() {
    let mut turtle = Turtle::new();

    let sides = 5.0;
    let distance = 200.0;
    let angle = 360.0 / sides;

    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
    turtle.right(angle);
    turtle.forward(distance);
}

Now, use loops to make it easier to read and less copy-paste.

use turtle::Turtle;

fn main() {
    let mut turtle = Turtle::new();

    let sides = 5.0;
    let distance = 200.0;
    let angle = 360.0 / sides;
    for _ in 0..30 {
        turtle.forward(distance);
        turtle.right(angle);
    }
}

As you can see, loops are very useful to avoid repeating. But also, don’t forget that the number of “loops” that happen doesn’t need to be fixed.

We can use variables, user input, and other stuff to determine for how long to loop.

Adding some comments

The programs are sometimes hard to understand, and it would be nice to leave some notes for the people reading it, so they can understand it too. And I bet you that you’ll forget what a program does after 3 months, even if you wrote it yourself. It happens to me too. So it’s good to have some notes on the program, so we can understand it later.

They can also be used to denote that some work is yet missing (we call these TODO) so it serves as a reminder for later on. But we all know that we will never get to do them. That’s how it works in reality, trust me.

fn main() {
   // Compute all the cells in a Chess Board:
   for row in 1..=8 {
       for column in 1..=8 {
           // TODO: For now just display the numbers, we'll fix this ""later"".
           println!("Row {}, Column {}", row, column);
       }
   }
 }

Most comments use the double slash //. When Rust sees this, it ignores any text on the right side of it, so we can use this to add our thoughts on the program.

However, if you need to comment out a lot of lines, adding // to every line can be daunting. VS Code has shortcuts for this (and you can customize them) so it’s easier.

But Rust also has comment blocks. A comment block starts with /* and ends with */. Rust will ignore everything in the middle, even if there are multiple lines.

/*fn main() {
   for row in 1..=8 {
       for column in 1..=8 {
           println!("Row {}, Column {}", row, column);
       }
   }
 }*/

Voilà. Now we no longer have any program. From Rust’s perspective, the file is empty.

But these comment types are a bit trickier. They cannot be nested. If you try to add a block comment on something that already contains a comment block inside, Rust will get confused.

Because of this, I prefer to avoid these and stick with the simple and reliable double slash (//).

What If…?

…it is raining outside? You should get an umbrella, right?

This is basically a conditional: If it’s raining, get an umbrella. We have these in Rust and use the keyword “if”. The name may not sound original at all, but helps to read the program as if it were English.

For example:

fn main() {
   let apples = 6;
   if apples > 1 {
       println!("You have many apples!");
   }
 }

This program will print You have many apples! only if the apples variable is bigger than one.

Of course, it only changes if you manually go and change the variable value. Don’t go that fast! We’ll see something useful soon.

The syntax is:


#![allow(unused)]
fn main() {
if condition { 
    // ... what to do if the condition is true ... 
} else { 
    // ... what to do if it's not ... 
}
}

Condition can be anything that is either true or false. Some examples:

  • apples == 1 → if apples is exactly 1.
  • apples != 1 → if it’s not 1. Any other value except one will do.
  • apples >= 1 → if it’s greater or equal to 1.
  • apples < 1 → if it’s less than 1.
  • apples <= 1 → if it’s less than or equal to 1.

Notice how I told you the if has an else part, but I did not write it on the above program. That’s because it’s optional. If we only care about the part it’s true, the else is not needed.

However, we cannot do the reverse. You cannot have an else without an if. If we need to target when the condition is false, we need to reverse the condition, so it returns true when we need to.

Let’s see an example with an else:

fn main() {
   let apples = 1;
   if apples > 1 {
       println!("You have many apples!");
   } else {
       println!("Please go to the supermarket.");
   }
 }

Ok, enough of this. I guess it’s too simple, and we need to spice it up with…

For loops again!

Yay! Wait, I thought you were excited about this.

Let’s do something useful. This program will graph the function \(y = x^2\frac{1}{20} - 9\) in the terminal:

fn main() {
   for y in -10..10 {
       for x in -30..30 {
           let value = x * x / 20 - 9;
           if value >= y {
               print!(" ");
           } else {
               print!("#");
           }
       }
       println!();
   }
}

As you can see, it only uses “for” and “if”. It might look complicated, but with a bit of work you should be able to follow it.

For the record, this is the output it produces:

$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
    Running `target/debug/learnrust`
                                                            
                                               
                      #########              
                    #############            
                   ###############           
                  #################          
                 ###################         
                #####################        
               #######################       
              #########################      
             ###########################     
            #############################    
            #############################    
           ###############################   
          #################################  
          #################################  
         ################################### 
         ################################### 
        #####################################
        #####################################

You can change the formula in let value = x * x / 20 - 9, and it will graph whatever math function you like.

I know, there’s a lot to unpack here. I’ll go step by step. But first, for the avid readers, yes the function appears mirrored upside down. This is because the first line that is drawn first is in math the bottom one, and computers draw top to bottom while in math the Y axis goes from bottom to top.

It can be fixed, but to keep things simple I prefer to keep this bug in.

Let’s go first on the inner code of the loops:


#![allow(unused)]
fn main() {
let value = x * x / 20 - 9;
if value >= y {
    print!(" ");
} else {
    print!("#");
}
}

This computes \(x²\) by doing x * x, divides by 20 and then subtracts 9. Rust has a pow() function to do \(x²\) instead of x * x, but to keep it simple I avoided it. Anyway, this gets stored into the value variable and the if compares against y.

So, if it’s less than y prints a hash #, if not, it prints a space otherwise.

This is meant to fill the shape. Given the math function:

\[ y = \frac{x^2}{20} - 9 \]

It computes which “squares” are below the math function and uses the # character to fill the shape.

Notice how I used print! instead of println!. The lack of the ln (which stands for line) makes that print not open a new line, so it prints to the right like a typewriter.

So all that is left is to loop across all x and y squares:


#![allow(unused)]
fn main() {
   for y in -10..10 {
       for x in -30..30 {
           // ... 
       }
       println!();
   }
}

So using here a nested loop we iterate through negative and positive values for both x and y.

As y is usually the vertical axis, and the terminal prints first left-to-right and then to-to-bottom, we need to first print an entire row; That’s why the for y appears first and the for x appears next.

After the for x is done, we need to move to the next line, so an empty println!() will move the cursor to the next line.

Other types of loops

So far we have seen for loops. Rust has other ways of doing loops that sometimes are better than for.

Loop

There is the infinite loop that is written by loop. But a program that has an infinite loop never ends and might become stuck forever. Therefore, when using these types of loops we need to make sure we break that loop at some point.

For example:


#![allow(unused)]
fn main() {
let mut counter = 1;

loop {
    if counter < 100 {
        counter *= 2;
    }
    if counter < 130 {
        counter += 1;
    } else {
        // important to include a `break` to stop the loop at some point.
        break;
    }
    println!("counter = {}", counter);
}
println!("ended with counter = {}", counter);
}

While

The other type of loop is while and does the same as loop but has a condition that must stay true to continue looping.


#![allow(unused)]
fn main() {
let mut counter = 1;

while counter < 20 {
    counter += 1;
    println!("counter = {}", counter);
}

println!("ended with counter = {}", counter);
}

It also allows to retrieve a value in-place on the comparison, like this:


#![allow(unused)]
fn main() {
let test = "Hello World!";
let mut chars = test.chars();

while let Some(c) = chars.next() {
    println!("c = {:?}", c);
}
}

text.chars() returns an iterator over the characters of test. Every time we call chars.next() it returns an Option<char> that can be either Some(char) or None.

The while loop will keep iterating until this function returns None. The result of the Some is stored in c, and we can print each character individually.

But I should insist on using for .. in where possible, because it tends to be more legible:


#![allow(unused)]
fn main() {
let test = "Hello World!";

for c in test.chars() {
    println!("c = {:?}", c);
}
}

Exercise: conditionals

Turtle: Improving with loops

Turtle: Making a spiral

Level: Apprentice

Now your spells are quite powerful already, and they loop around and do some logic. This, as you have seen, creates full programs that start to make sense and are beginning to be useful.

In this phase of learning you’re going to learn what is needed to finally make useful programs. From here your programs would be good enough that you’ll be able to make your own utilities for yourself.

The code might not be pretty, but you’ll have enough tools to do a lot of interesting stuff.

You can consider this chapter to be learning your first fireball spell. Rough, but powerful and effective.

Introducing Functions

It might seem difficult to picture that sometimes the same piece of code needs to repeat in different places of the code, and it’s not a loop what I’m talking about.

The example programs I can add in this book have to be small, so we can go line by line. In reality, good useful programs have thousands of lines. It’s hard to make something really useful in twenty lines with only the basic operations we learned.

It might seem daunting to think about writing several thousand lines, but it is easier than it seems. We always start small, we keep adding pieces and after a few weeks it’s easy to have those thousand lines.

One of the applications I wrote, zzping, has nearly 7000 lines of code in it. And it has been only a hobby without much investment from my side. Programs get big very easily.

Having all those instructions inside the fn main() {...} is very hard to follow. It’s similar to organizing stuff in your room or in the house. If all items of your house were in a single big box, trying to find anything there is nearly impossible, so we all use different drawers, stands and boxes to sort the stuff, so we can locate it later.

In the same fashion, we split big programs into different files, so all related instructions that work towards a similar goal are near each other, and each file has its own tools there.

We will see later on how to split into different files (in Rust, those are called modules), but now I want to explain how to sort stuff out inside a single file.

Imagine we’re doing some sort of program that tells the user interesting stuff, and it has a menu:

fn main() {
   println!("Welcome to the Trivia program!");
   println!("------------------------------");
   println!("");
   println!("Please choose an option:");
   println!("");
   println!("    1. Tell a funny story");
   println!("    2. Show the multiplication table for a number");
   println!("    3. Show the dividing table for a number");
   println!("    4. Tell the future for a zodiac sign");
   println!("    5. Browse the cooking recipes");
   println!("    6. Exit the program");
   println!("");
   println!(" Your option:");
 }

As you can imagine, each of these options almost consists of its own program. Trying to code everything in here is going to be really confusing:

fn main() {
   println!("Welcome to the Trivia program!");
   println!("------------------------------");
   println!("");
   println!("Please choose an option:");
   println!("");
   println!("    1. Tell a funny story");
   println!("    2. Show the multiplication table for a number");
   println!("    3. Show the division table for a number");
   println!("    4. Tell the future for a zodiac sign");
   println!("    5. Browse the cooking recipes");
   println!("    6. Exit the program");
   println!("");
   println!(" Your option:");
   let option = 1; // TODO: Read the user input option and store it here.
   if option == 1 {
       println!("Here's a joke...")
       // TODO: Add jokes and a joke selector
   }
   if option == 2 {
       // TODO: Ask the user which number
       // ... compute and show the multiplication table
   }
   if option == 3 {
       // TODO: Ask the user which number
       // ... compute and show the division table
   }
   if option == 4 {
       // TODO: Add a database of zodiacs with their predictions
       // ... ask the user and show the matching one
   }
   if option == 5 {
       // TODO: Add a database of recipes
       // ... ask the user and show the matching one
   }
   if option == 6 {
       // TODO: Exit the program here
   }
   // TODO: Loop again to the beginning if option 6 wasn't chosen.
}

It would be nice if we could break the program into subprograms that do specific things, so we can call those when we need them, right?

That concept is exactly what a function is.

As usual, I’ll go back to short and stupid programs, but keep in mind the above example where they’re useful.

fn welcome() {
   println!("Welcome adventurer!");
   println!("There are lots of treasures hidden in this place.");
   println!("Oh, and there's also a princess trapped in a castle.");  
   println!("");
   println!("You know what to do.");
}

fn main() {
   welcome();
   println!("End of program");
}

Here we can see a “welcome” function that prints six lines of text. What the computer does is:

  • It always starts from the main function.
  • Reads the welcome() call to the function, so it jumps to the top fn welcome()
  • Executes the five println!
  • The function ends on the }, so it goes back where it was on main.
  • Reads the next line and prints “End of program”.
  • Reaches the end of main (the }) and the program ends here.

A function has two sides, the declaration and the call. The function declaration states what is the function name and what it does:


#![allow(unused)]
fn main() {
fn your_function_name() { 
  // ... your code here for what this function does ... 
}
}

And then you can call it as many times you want, using:


#![allow(unused)]
fn main() {
your_function_name();
}

And the key thing here is “call it as many times you want”. The function’s purpose is to be reused several times, so you don’t have to repeat your code several times.

The declaration part doesn’t need to be above or below the main(), it can be anywhere as long as it is not inside the main.

Placing it before or after the main is fine, the order doesn’t matter.

Returning values from functions

It is also possible to use a function to calculate a certain value. For example, we can calculate the number PI (\(\pi\)):

fn calculate_pi() -> f64 {
   // https://en.wikipedia.org/wiki/Leibniz_formula_for_%CF%80
   // pi/4 = 1 - 1/3 + 1/5 - 1/7 + 1/9 - ...
   let mut pi_4 = 1.0;
   let mut divisor = 3.0;
   let steps = 20_000_000;
   for _ in 0..steps {
       pi_4 -= 1.0 / divisor;
       divisor += 2.0;
       pi_4 += 1.0 / divisor;
       divisor += 2.0;
   }
   return pi_4 * 4.0;
}
fn main() {
   let pi = calculate_pi();
   println!("pi: {}", pi);
}

Note that it’s quite hard to understand how these formulas actually compute PI. You’re not alone, and this is not the purpose of this exercise. We’re only focusing on a function that returns PI, and we don’t care on how it’s done.

Again, this program might be a bit too much, but the important thing is in the first line:


#![allow(unused)]
fn main() {
 fn calculate_pi() -> f64 {
}

The arrow symbol -> indicates that this function returns a value. f64 is which kind of value we want to return, which for this case is a floating point number (a number with decimal places). If we wanted to return an integer we would have used i64.

The last line of the function specifies the actual value to be returned:


#![allow(unused)]
fn main() {
   return pi_4 * 4.0;
}

Since this fraction series calculates \(\pi\) divided by four, we need to multiply it by four to get PI, and the return keyword is what signals Rust that it should exit the function at that point (it marks the end of it) and return the value on the right.

It is worth noting that while the return keyword is in most other programming languages as well, Rust specifically has another way of writing this:


#![allow(unused)]
fn main() {
   pi_4 * 4.0
}

Just removing the return keyword and also removing the semicolon signals Rust that this is the output of that block. It behaves similar but not identical to return. For simplicity’s sake we’ll keep using return for now which could be simpler to read and understand. However, this form is actually preferred in Rust code. We will learn it in depth later on.

The next thing we should focus on is how this function is called:


#![allow(unused)]
fn main() {
  let pi = calculate_pi();
}

Notice how it is used like it was a value. Rust will call the function, get the resulting number and replace here the function by the output value. Then it will be stored in the variable pi.

Everything else in this program is something that we already saw before. Let me go over a few things that might seem new:


#![allow(unused)]
fn main() {
  let mut pi_4 = 1.0;
  let mut divisor = 3.0;
  let steps = 20_000_000;
  for _ in 0..steps {
}

We are using let mut here to be able to change the values of pi_4 and divisor inside the function. Without that, Rust will not let us change the variable contents.

Another thing that might seem strange is the number 20_000_000. This is just 20 million, but the underscores are placed to make it easier to read. They have no meaning to Rust.

In the for loop, you’ll notice that it says 0..steps; so this basically makes the for count up to steps. There’s nothing special with this.

Finally, the for has as a variable underscore _. This is because this variable is not used. We only need N steps, but we use divisor to keep track of the position. Rust will emit warnings for unused variables. To avoid this, we use an underscore to signal Rust we don’t need this value.

The result of the program is:

pi: 3.1415926785904635

It’s quite close to the real thing. Of course, if you increment the number of steps it will get closer, but the program will take a bit to run. For those cases we can make our Rust program faster by adding the --release flag to cargo run:

 $ cargo run --release
   Compiling learnrust v0.1.0 (/home/deavid/git/rust/learnrust)
    Finished release [optimized] target(s) in 1.00s
     Running `target/release/learnrust`
  pi: 3.1415926785904635

Programs are usually built in “debug” mode, which compiles faster, but the program runs slower. This is the preferred mode for working with our program. But when distributing our application, the “release” mode is best, as it will make the program perform at its best.

Function arguments

But what if we wanted a configurable number of steps? We can do that!

Functions can accept data when they’re called, like this:


#![allow(unused)]
fn main() {
let pi = calculate_pi(100);
}

But for this to work we need to change the function signature:


#![allow(unused)]
fn main() {
fn calculate_pi(steps: i64) -> f64 {
}

We have to specify between the parenthesis what is the variable name that will record the input value, as well as the type. Since steps won’t have any decimal places, we use “i64” instead of “f64”.

Therefore, a more complete description on the syntax for a function is:


#![allow(unused)]
fn main() {
fn function_name( input_var_name: type ) -> return_type { 
    // Function code here... 
}
}

Where the arguments can be omitted, and the return type can be omitted as well if we don’t use them.

It is possible as well to have many input values, just separating them by comma:


#![allow(unused)]
fn main() {
fn function_name( input1: type1, input2: type2, input2: type2 ) { 
    // Function code here... 
}
}

Each input variable needs to have its own type associated. We’ll see more examples on this later on.

For now, let’s see the full program changed:

fn calculate_pi(steps: i64) -> f64 {
   // https://en.wikipedia.org/wiki/Leibniz_formula_for_%CF%80
   // pi/4 = 1 - 1/3 + 1/5 - 1/7 + 1/9 - ...
   let mut pi_4: f64 = 1.0;
   let mut divisor = 3.0;
   for _ in 0..steps {
       pi_4 -= 1.0 / divisor;
       divisor += 2.0;
       pi_4 += 1.0 / divisor;
       divisor += 2.0;
   }
   return pi_4 * 4.0;
 }
 fn main() {
   for n in 1..10 {
       let steps = i64::pow(10, n);
       let pi = calculate_pi(steps);
       println!("pi: {} ({} steps)", pi, steps);
   }
 }

This program now has a loop in main() that will do 10 different calculations of PI at different precision levels.

Here’s the output:

 $ cargo run --release
   Compiling learnrust v0.1.0 (/home/deavid/git/rust/learnrust)
    Finished release [optimized] target(s) in 1.00s
     Running `target/release/learnrust`
 pi: 3.189184782277596  (10 steps)
 pi: 3.1465677471829556 (100 steps)
 pi: 3.1420924036835256 (1000 steps)
 pi: 3.1416426510898874 (10000 steps)
 pi: 3.141597653564762  (100000 steps)
 pi: 3.1415931535894743 (1000000 steps)
 pi: 3.1415927035898146 (10000000 steps)
 pi: 3.1415926585894076 (100000000 steps)
 pi: 3.1415926540880768 (1000000000 steps)

It’s important to run this with --release or it will take a bit too long to compute.

Also, it’s always mesmerizing to see how more and more digits are getting “stuck in” as the number of steps grows.

I think this variation should be self-explanatory with maybe the exception to this line:


#![allow(unused)]
fn main() {
       let steps = i64::pow(10, n);
}

This computes \(steps = 10^n\), that is, the power of 10 raised to the “n”. For this we use the function pow that exists inside the i64 type. The double colon operator a::b is used to access the functions inside other libraries (or any content in fact).

Note that this function pow accepts two numbers as input and returns a number.

Data types

We have reached a point where I cannot continue to explain much more without giving you a fair bit of theory. Sorry about this, but it’s needed.

What are data types?

First: what is data? Data is just the technical term for value. The number 5 is a value and is data, and so is 2.41 or the string “Basement”.

The concept of “data types” just refers to the different kinds of values. You see, 3+5 is 8, but “Base”+“ment” is “Basement”. The sign plus (+) might work differently depending on if it is text or a number.

Some operators like dividing (/) make sense on numbers (8/2=4) but don’t make any sense on texts (“Base”/“ment” = ????).

So the primordial thing that puts data types apart is what can be done with the values, what makes sense and what doesn’t; and also what the operations actually mean.

Because, you know, computers might look like really smart, but they’re actually pretty stupid. If you do, “3”+“5” is just “35” and not “8” (because of the quotes, it is treated like text, so it gets concatenated when using the plus sign). They take everything literally, too literally. And we need to tell them what to do, step by step to a level that feels like training a monkey how to make a chocolate brownie.

There are tons of data types, you can construct your own, and you can also use other people’s types. But in general, to start, we have three basic types:

  • Numbers
    • Integers (which can be positive or negative, but don’t have a decimal point)
    • Floating point numbers (or floats) that actually have decimal places.
  • Texts, generally called “strings” in programming (at some point later I might explain where the name “string” comes from, it’s a bit funny)
  • True/False types, called “boolean”, used to store the result of comparisons such as “apples > 1”.

You might be inclined to always use floating point numbers for everything, as they can do much more than regular integers. If they’re better, there’s no point in using the puny integers, right?

Well, no. Later on I will point out the different problems that floating points have. For now, just write 2.0 instead of 2 for every number, and you’re done.

Rust data types

Rust data types are however a bit more nuanced. You see, these values we want to store have to be actually recorded somewhere in your physical computer. And that will be your computer memory (RAM, or DDR if you prefer). The number 4 might not look like much, but Rust has to ask for memory from your computer in order to save the value somewhere, else it would be lost, forgotten and programs won’t run.

So the question that Rust faces is: how much memory is needed for this variable? A variable can change its value later on, for example that x=4, later on it can be x=99999999. It needs to grab enough memory in order to ensure that all values you might want to put there actually do fit.

This is why integers are split into different sizes:

  • i8
  • i16
  • i32
  • i64

Each of those is a valid Rust type for an integer. And yeah, the “i” is short for integer. The number that it follows is the number of bits that Rust will get for them.

Is 8 bits a lot? Let’s see. A computer might have 4 gigabytes of RAM. Giga just means billion. So that’s 4,000,000,000 bytes. And a byte is just 8 bits.

So this means that i8 only uses 1 byte of memory. And if a computer has 4,000,000,000 bytes, I can surely assume it can store a lot of those and still have plenty of space left. So yeah, it’s very tiny.

But this affects the range of numbers we can store. It is fine for storing -4, 12 or 81. But 355 is too much for an i8 and the program will fail. If we need to store bigger numbers, we need to use types that use more space.

For example, i16 can store up to 32,000.

But you know what? This is too complicated. Why bother? Use i64 and that’s it. Forget about the others for now and just use i64 for everything. Your computer has so much memory that it doesn’t care if a number uses 1 byte or 8.

An i64 can store any integer number up to roughly 9,000,000,000,000,000,000. Either positive or negative. Unless you want to start counting atoms, I think we have more than enough to work with. So again, let’s use i64 for all our integer numbers and forget about this conversation.

Rust also has unsigned numbers, which work exactly the same as the regular integers, but they can’t store negative numbers, Instead of “i” they go with “u” and work exactly the same:

  • u8
  • u16
  • u32
  • u64

Why limit ourselves to only positive? Well, they can hold double the numbers inside. But… why? Wasn’t 9,00… or whatever enough for us…?

Exactly. It is enough. So again, we won’t be using these. Use i64.

There are two extra integer types in Rust:

  • usize
  • isize

If your computer is a 64bit one, then these are equivalent to u64 and i64. But these have their specific use. We will come back later to the “usize” as it is how computers locate things in memory. Certain things such as locating an item in a list/collection of numbers use “usize”.

I’m dumping this to you now to give an approximate picture of the data types that Rust has in the hopes that you’ll begin to recognize these later on. I’ll explain this again later on with more detail, so no need to study and memorize these things now.

Floating points, very similarly have two variants for sizes:

  • f32
  • f64

“f” for float, and 32 or 64 for the amount of memory that they use. And guess what, we’ll use f64 when we need decimal places and forget about f32.

Boolean values (bool) only have one type:

  • bool

There isn’t much secret on this type, it is quite like an integer that holds 0 for false and 1 for true. That is all.

Strings (texts) also come in two variants, but, surprise! It’s not for size!

  • str
  • String

The difference is a bit too nuanced to explain right now, so let’s just say they’re almost the same thing. We will use String for our variables, as it is simpler to use, but in some cases, Rust wants a “str” instead. They can be converted one to another, so it’s not a big deal.

As you might have noticed, they don’t have any specific size. So, how big can the text be inside?

Short answer: As long as you want.

They’re dynamic. Rust will request more memory when needed. So as long as your computer has enough memory to store the text, it will work1.

And those are all types we will be using for a long, long time here. So, to recap:

  • Integer numbers (without decimal places): i64
  • Floating point numbers (with decimal places): f64
  • Strings (text): String (with the first S in uppercase)
  • Boolean (true/false): bool
  • Locating values inside a list: usize (we’ll see this later on)
1

Oh crap, I’m sorry, I’m lying way too much… I swear I will fix this mess later on. For now, bear with me, this is the explanation you need

Back to declaring variables… with type

Now that we have a rough idea on types, we can do more things with the “let” keyword.

For example, we can tell Rust to use an i64 for our number:


#![allow(unused)]
fn main() {
   let x: i64 = 2;
}

That will force the selection of this type. But wait… if we didn’t specify a type, and Rust needs one to know how much memory to use… how did Rust make the previous examples work?

As a reminder, we did:

   fn main() {
      let x;
      x = 4;
      println!("{}", x);
   }

And we did not specify a type here. In fact, this could be i8, i16, i32, i64 or even u8, u16, etc. All these will work.

Which one did select Rust and why?

Did we care which one to use when writing this? No. And neither does Rust.

Rust will do something similar to what I’m telling you to do. i64 is a perfectly fine type for most uses, so just go with that. Rust is probably doing the same thing here: It seems you want a number; does i64 work? Ok, so that one then; that looks good1.

Rust does a lot of guesswork to select the type, and most of the time we don’t need to care about this. In a few particular times it might be confused with too many options that are too different, and might complain. In such cases, we need to help it by defining the type. Aside from that we’ll avoid setting the types and let Rust do its magic.

1

Note here that Rust might guess differently and prefer an i32 over an i64 “because it’s smaller, and it still works”. As we didn’t care about which type, it’s pointless to try to know what Rust exactly does. If we want to be sure it’s an i64, we just write it down.

dbg!() and format!()

Printing in console is nice. But sometimes we want just to quickly see what are the contents of a variable or operation.

For this we can use the dbg!() macro1:


#![allow(unused)]
fn main() {
let value = 123.0;
dbg!(value);

dbg!(1.5 * 3.2);
}

This prints:

[src/main.rs:4] value = 123.0
[src/main.rs:6] 1.5 * 3.2 = 4.800000000000001

This is quite useful! Avoids writing a lot and does what we need to understand what the code is doing at different points.

You’ll notice that the result of the multiplication is not \(4.8\). This is fine. Floating points have problems with precision and it shows. Soon enough I’ll explain. Don’t worry too much.

format!()

Here’s another case. Sometimes we want the ability that println!() gives us regarding on composing a string, but we don’t want to print it. We want to store it.

For this, we have the format!() macro, that works exactly as println!() but instead of printing it returns the result as a string:


#![allow(unused)]

fn main() {
let text = format!("your lucky number is {:05}. Try another day!", 7);

dbg!(text);
}
[src/main.rs:5] text = "your lucky number is 00007. Try another day!"

Nice!

This can be used to compose texts and pass it to other functions.

You can see more examples here:

https://doc.rust-lang.org/rust-by-example/hello/print.html


1 Looks like a function, you can think of it as a function, but to be correct, things that end with ! are called Macros. We won’t see how to make these until very late in (if at all), as you don’t need to make macros to code 99.999% of programs.

Converting types with explicit conversions

As you can imagine, it is pretty common having a mix of types. With so many data types it is easy to end up having an operation that needs to mix them in some way, and Rust does not allow that.

We will avoid the majority of those problems by using as few types as possible, just i64 and f64. But it will happen at some point.

For example, consider a simplistic example:


#![allow(unused)]
fn main() {
   let apples: i64 = 2;
   let apple_weight: f64 = 0.35;
   let total_weight: f64 = apples * apple_weight;
}

In this case we could define apples to be a float, and it will work, but this could appear differently where this is not so simple, and we need “apples” to be an integer regardless.

To make this work we can simply convert “apples” variable in the spot, right where we need it:


#![allow(unused)]
fn main() {
   let apples: i64 = 2;
   let apple_weight: f64 = 0.35;
   let total_weight: f64 = apples as f64 * apple_weight;
}

By adding as f64 we’re making what is called an “explicit conversion”. Rust will convert the integer into a floating point on the spot and will use that on the calculation of total_weight.

The variable apples is still an integer after that line. If we keep using it as a float we would need to keep converting every time. So if it is needed in all places as a float, maybe it is better to just make it a float from the start.

Explicit conversions have their limitations though. It works only between some types that are easy for Rust to convert. You can’t use it to convert to or from strings (there are other conversion types for that), or between string types.

Doing the reverse, converting a float into an integer, will drop any decimal places. So 2.99999 as i64 is just 2. It will not round the number to 3. There are other ways for rounding numbers.

The reason why explicit conversion works for so few cases and its behavior: These conversions are usually supported by your CPU and are really fast. Rounding numbers or converting an integer into a string will take several iterations in your CPU to complete. Those are also very fast, just not as fast as the explicit conversion. Rust is trying to signal you where your program might be consuming more resources, which is helpful, but we don’t need to be obsessed with speed. Rust is very fast, and so is your computer (even if old).

Exercise: explicit conversions

Now, use what you learn about doing x as f64 to fix the following program:


#![allow(unused)]
fn main() {
let x = 4
let y = 2.0;
let z = x / y;
dbg!(z);
}

We could solve in two directions:


#![allow(unused)]
fn main() {
let x = 4
let y = 2.0;
let z = x as f64 / y;
dbg!(z);
}

Or:


#![allow(unused)]
fn main() {
let x = 4
let y = 2.0;
let z = x / y as i64;
dbg!(z);
}

A tiny bit on strings

Storing and manipulating texts in a program is a bit harder to explain than numbers. This is because a computer does not work with text, but with numbers.

Surely you have heard that in a computer everything are ones and zeros (the binary system). These can be combined to create big numbers, and with some tricks the CPU can also work natively with floating point in order to have decimal places.

But text is not a number, which means that Rust needs a bit of work behind the scenes to make these work.

As a basic introduction, a text is nothing more than a sequence of letters. The text ABC might be a string, but it also is a sequence of three letters: A, B and C.

Each letter is a character. If we assign each possible character a number, we can in fact store text using numbers:

  • A → 65
  • B → 66
  • C → 67

This is totally arbitrary, but there’s a standard called “ASCII” that defines a table of conversions from letters to numbers. If all programs use the same table1, then we can save those numbers into a file, and get the correct texts back when another program reads the same file.

So a string is just a list of numbers that it is conveniently interpreted as text.

Instead of the ASCII table Rust by default uses UTF8 for strings. I won’t go into much detail here, but the world has different languages with different characters. Think about Chinese, Japanese, and Russian. The ASCII table is not good for those. UTF8 is still compatible with ASCII, but it allows for a character to span multiple number entries (bytes), so we can have in the same format any kind of writing language.

To keep things simple, just assume it is ASCII for now. We’ll cover UTF later on.

Why are texts called “strings”? https://stackoverflow.com/questions/880195/the-history-behind-the-definition-of-a-string (To be completed later)

1

Actually they don’t, and that’s why we have so many problems with tildes, foreign characters, and the reason why UTF exists.

Simple explanation on &str vs String

We will use these soon, so they need a bit of an introduction. They both store a sequence of characters.

It all boils down to what I commented early on. Rust (and the computer itself) needs to know how big a variable is. But a string can change in size very abruptly.

Because of this, Rust cannot know the size of most strings until the program is actually executed. This poses a problem, because without a rigid size in memory we cannot have variables that contain text. At most, we can have a variable that stores a single character, but this isn’t very useful.

So what do we do? Well, instead of having the text itself in a variable we store where the string is and its length. This is the type &str.

While this is the fastest way for the computer to do this, and convenient for some parts of the program, it’s certainly very hard to manipulate. It’s nearly impossible to change the contents of a &str variable.

It’s because of that inconvenience that String exists. The String type encapsulates and automates string changes.

So, in general, when a text never changes, &str is just fine. But if we want to store the user’s input, String might be better.

In Rust these two are very easy to convert from one to another:

 fn main() {
   let s1 = "This is a &str";
   let s1a = s1.to_owned(); // This converts &str to String.
   let s2 = String::from("This is a String");
   let s2a = s2.as_str(); // This converts String to &str.
 }

We’ll go on more details later on, but for now we will basically use whatever type Rust is happier with. Depending on what we’re doing we will probably need one or the other and convert accordingly.

Note that there are lots of functions to create a String from a &str:


#![allow(unused)]
fn main() {
let s1 = "text".to_owned();
let s2 = "text".to_string();
let s3 = String::from("text");
}

They all do the same. In programming, we try to avoid having two things for the same, but in this case, Rust has to have three variants, because they can be used in different contexts.

  • x.to_owned() means to create an owned version of the variable; that is, to remove the & character (borrow), which we will be learning soon.
  • x.to_string() means to convert something into a string. This is equivalent to format!("{}", x).
  • String::from(x) means to create a string from something else. It can only be used with &str, String or other types that actually represent text or at least a character.

Is any of those faster? Maybe. Probably String::from(). But I bet it cannot be measured in 99.9% of real Rust programs. The difference is too small to actually matter.

A bit more detailed explanation on functions

A function has always two parts, the declaration, and the call.

The declaration is this part with the fn keyword:


#![allow(unused)]
fn main() {
fn one() -> i64 {
    return 1;
}
}

Where the call is this code:


#![allow(unused)]
fn main() {
add_one();
}

You can have functions declared that are not called (you’ll get a warning), but you cannot have a function called that is not declared.

Also, we can call the same function as many times we want. It is the whole point of functions to be able to be called thousands of times.

To execute/call a function, we write the name and follow it by parenthesis:


#![allow(unused)]
fn main() {
add_one();
}

If we forget the parenthesis, the function will not be called:


#![allow(unused)]
fn main() {
add_one;  // WRONG!
}

The return value of a function can be used as a regular value:


#![allow(unused)]
fn main() {
let two = 1 + one();
let three = 1 + one() + 1;
}

Rust will execute the code in the function, get the return value and “replace” the function call with that value:


#![allow(unused)]
fn main() {
let two = 1 + (1);
let three = 1 + (1) + 1;
}

Function names are actually variables. We won’t be using this “feature” in this book, but it’s good that you know this is possible:


#![allow(unused)]
fn main() {
let one_fn = one;

let two = one_fn() + 1;
}

These are called function pointers. This is why it’s important you don’t forget the parenthesis, as the error might get you confused. Rust will understand that you want to store the function code somewhere1.


1

Actually they are function pointers, they point to the memory address where the code sits. But as usual, this goes outside what I want to explain.

They’re black boxes

We need to understand functions as a black box. Something goes in, something goes out. We don’t care that much on what happens inside when we call it.

You can imagine them as a microchip, that has pins for getting data in, and pins that give data out:

              ┌——————————————————┐
 number_1  -->│                  │
              │                  │
              │  add_numbers()   │---> result_number
              │                  │
 number_2  -->│                  │
              └——————————————————┘
              

This equates to this function:


#![allow(unused)]
fn main() {
fn add_numbers(number_1:i64, number_2:i64) -> i64 {
//     │          │             │              ↳ Return value type
//     │          │             ↳ Second argument name and type
//     │          ↳ First argument name and type
//     ↳ Function name
}

Remember: You can have functions that don’t return a value. A function that accepts one, two, or many arguments. Or both.

This function doesn’t need any arguments, and doesn’t return anything:


#![allow(unused)]
fn main() {
fn help() {
}

This one accepts one argument but doesn’t return anything:


#![allow(unused)]
fn main() {
fn manual(page: i64) {
}

This one doesn’t have arguments, yet has a return value:


#![allow(unused)]
fn main() {
fn calculate_pi() -> f64 {
}

Omitting “return”

In Rust, most of the time we don’t need to add return to return a value. If the last statement of the function lacks a semicolon, implies return.

For example:


#![allow(unused)]
fn main() {
fn add_numbers(a: i64, b: i64) -> i64 {
    let c = a + b;
    return c;
}
}

Can be simplified as:


#![allow(unused)]
fn main() {
fn add_numbers(a: i64, b: i64) -> i64 {
    let c = a + b;
    c
}
}

Which in turn, can be simplified again:


#![allow(unused)]
fn main() {
fn add_numbers(a: i64, b: i64) -> i64 {
    a + b
}
}

Nice!

Types as matching shapes

Remember those kids toys where you have to put the rectangle on the rectangular hole, and the circle in the circular hole?

Data types are somewhat like this.

Imagine that the types were shapes:

  • Integers are squares: ■
  • floating points are circles: ◉
  • Strings are parallelograms: ▰

Now, when you make a function, the arguments are like “sockets” you must connect:

              ┌——————————————————┐
 number_1 ○-->│                  │
              │                  │
              │  add_numbers()   │◉---> result_number
              │                  │
 number_2 ○-->│                  │
              └——————————————————┘             

A function that accepts several types would look like this:

   ┌————————┐
--▢|        |
   |        |
--◎|        |
   |        |
--▱|        |
   └————————┘             

You need to provide the right shapes to that box to get it working.

Or in specific terms, you need to provide the exact types the function is asking for to be able to call it.

You cannot call a function that accepts a f64 with an i64.

In the same sense, if your code is expecting an i64 as a result, the function must return that:

fn calc_pi() -> f64 {
    // code here...
    return pi;
}

fn main() {
    let pi:i64 = calc_pi(); // Error! calc_pi returns f64 but the variable is i64.
}

Returning multiple values

We can return more than one values by using tuples. A tuple is basically several values packed together. We put in between parenthesis, and each value separated by comma.


#![allow(unused)]
fn main() {
fn default_position() -> (f64, f64) {
    return (0, 0);
}
}

To use this, we can just unpack the values upon receiving:

fn main() {
    let (x, y) = default_position();
}

If we need to specify types, we can do for each variable:

fn main() {
    let (x: f64, y: f64) = default_position();
}

But we can also receive the return value as a single tuple:


#![allow(unused)]
fn main() {
let z: (f64, f64) = default_position();
}

To use this tuple, we can access the data in order by doing z.0 or z.1:


#![allow(unused)]
fn main() {
let z: (f64, f64) = default_position();
let x = z.0;
let y = z.1;
}

There’s not much mystery to this. Tuples are good to have several values packed together. And they’re used mainly for this use case, to get multiple return values in a function.

Introduction to Closures

In Rust, it is possible to create a function without using the fn keyword.

Why would we want to have a secondary way of creating these?

Well, sometimes we need a function for something that appears just once.

Sometimes these functions are very small.

But still, this doesn’t explain why we need a secondary way of creating functions.

The real reason though, is that some functions require functions to be passed as arguments.

Wait what? You mean to call a function in a function argument?

Yeah. Nope. That’s not it. I mean the function itself, the name, before being called.

Imagine we want a function that counts stuff. But what it counts, is up to the caller to decide.

For example, given the text:

The fox jumped over the lazy dog.

We want to count the letter e.


#![allow(unused)]
fn main() {
fn count_e(text: &str) -> i64 {
    let mut count = 0;
    for c in text {
        if c == 'e' {
            count += 1;
        }
    }
    count
}
}

Okay, good. But if we want to be able to count any character that we want?

Simple, add another argument!


#![allow(unused)]
fn main() {
fn count(text: &str, what: char) -> i64 {
    let mut count = 0;
    for c in text {
        if c == what {
            count += 1;
        }
    }
    count
}
}

Yeah, we don’t see the problem yet.

But now, imagine we want also to be able to count uppercase values.

What value do we give to this function, so it can compare against many values?

We can pass a function itself!

fn count(text: &str, what: fn(char) -> bool) -> i64 {
    let mut count = 0;
    for c in text.chars() {
        if what(c) {
            count += 1;
        }
    }
    count
}

fn uppercase(c: char) -> bool {
    if c >= 'A' && c <= 'Z' {
        return true;
    }
    return false;
}

fn vowel(c: char) -> bool {
    match c {
        'a' | 'e' | 'i' | 'o' | 'u' => true,
        _ => false,
    }
}

fn main() {
    let text = "The fox Jumped over the Lazy Dog.";

    let c = count(text, uppercase);
    println!("uppercase = {}", c);
    let c = count(text, vowel);
    println!("vowels = {}", c);
}

This prints:

uppercase = 4
vowels = 9

Because we can have such functions (and the Rust library contains a few of those) we might need to create functions just to be able to call these.

And it can get cumbersome.

For example, Rust has a function char::is_alphabetic that returns true if the character is a letter.

However, we might want to reverse it:

fn is_not_alphabetic(c: char) -> bool {
    !c.is_alphabetic()
}

fn main() {
    let is_not_alphabetic2 = |c: char| !c.is_alphabetic();
    
    dbg!(is_not_alphabetic('0'));
    // prints --> is_not_alphabetic('0') = true
    dbg!(is_not_alphabetic2('0'));
    // prints --> is_not_alphabetic2('0') = true

    dbg!(is_not_alphabetic('a'));
    // prints --> is_not_alphabetic('a') = false
    dbg!(is_not_alphabetic2('a'));
    // prints --> is_not_alphabetic2('a') = false

}

In the above code we can see already the new way of creating functions.

These are called “Closures” in Rust. In other languages these are called “Lambdas”.

A closure has the syntax:


#![allow(unused)]
fn main() {
|argument1:type1, argument2:type2| { code; code; code; return value; }
}

You can have a function that accepts no arguments:


#![allow(unused)]
fn main() {
|| { code; code; code; return value; }
}

And as usual, you can omit the return by skipping the semicolon:


#![allow(unused)]
fn main() {
|| { code; code; code; value }
}

Finally, if it’s only one statement, we can skip the curly brackets altogether.


#![allow(unused)]
fn main() {
|| value
}

For example:


#![allow(unused)]
fn main() {
|| 3.51
}

Or:


#![allow(unused)]
fn main() {
|| println!("Here's a text")
}

These are all functions. But you need to store them into a variable or pass them into a function in order to use them. This syntax, by itself, does nothing.


#![allow(unused)]
fn main() {
// This makes sense: (gives it a name)
let f1 = || println!("Here's a text");
// we call it as 'f1()'
f1();
// This does not make sense:
|| println!("Here's a text");
// doesn't have a name, can't be called.
}

Or a more advanced example could be:


#![allow(unused)]
fn main() {
let c = count(text, |c| matches!(c, 'a'|'e'|'i'|'o'|'u'));
}

We just created a function that returns true for vowels in less than one line.

This is quite powerful, and it’s okay to feel overwhelmed on this for now. I won’t be writing code like this, so you don’t need to truly understand all these for now.

But it’s important that we know that these exist.

Project: A simple game with Macroquad

Macroquad is a simple game engine aimed for beginners that only requires minimal knowledge of Rust:

https://crates.io/crates/macroquad

We will use this game engine only for this game, then we will shift to something more powerful. This means that in the process you’ll have to learn the API of this to never use it again, but in fairness, it is pretty simple.

The alternative would be to not do this project now and keep reading theory. But that’s not fun, and we should put the current knowledge into use, so it becomes the foundation for the next things that will come.

Survival pong

A pong game where the player just makes the ball bounce against the wall. It scores for each wall hit (even top-down walls).

Every time the ball hits the paddle, it speeds up a bit.

Logically, the tactic has to be to make each shot hitting as many walls as possible before returning to the paddle.

Level: Adept

Now we’re getting serious. Hope you did as many exercises as you could, because we’re reaching a level that is high enough to compare to most books.

From here, you should be able to follow other books too to reinforce this learning.

We will push in this stage basically from zero to 100. I’ll get you into a minimal base, so you will be able to do useful programs after this part.

Magic? Arcane? Not anymore. We’ll get understanding on most concepts in programming, so they will stop sounding like black magic.

Let’s use libraries and create programs that perfectly could run in a production environment.

NOTE: It’s possible that I’m pushing this section too far. I’m aware that I am covering too much ground here. Depending on how this plays out I might split this into two.

Modules - splitting files

Having all the program into a single file stops being convenient as the program gets bigger and bigger. One hundred lines are fine, 1000 maybe, but 5000 is definitely too big to handle.

The problem is as the file grows we easily lose track of things, in which parts they were and so on.

The whole folder that cargo new created for us is called a project. So if we recall when we created the initial project:

$ cargo new learnrust
     Created binary (application) `learnrust` package

But so far we have been adding different binaries, and with that we have been creating different files.

src
├── bin
│   ├── enums2.rs
│   ├── enums.rs
│   ├── hashmap.rs
│   ├── print.rs
│   ├── sample1.rs
│   ├── structs.rs
│   ├── turtle_loops.rs
│   ├── turtle_vars.rs
│   └── variables.rs
└── main.rs

However, these are not modules! Those are binaries.

Modules can be reused between different files while binaries can only be compiled as full programs. We cannot get a function from a binary into a different one.

To share functionality we need to move it into modules.

For example, let’s say we need something to be reused. Maybe we want a file that contains units of measure.

So we’ll create a new file called src/units.rs.

And we add a few units of length. Or all of them now that we’re on this:


#![allow(unused)]
fn main() {
pub enum LengthUnit {
    Kilometer,
    Meter,
    Millimeter,
    Inch,
    Angstrom,
    Mile,
    Furlong,
    Chain,
    Rod,
    Fathom,
    Yard,
    Foot,
    Parsec,
    LightYear,
    AstronomicalUnit,
}

}

Yay! We got a new module. Or do we?

So in order to be a full module we should be importing this from another file.

For src/main.rs we can add the following line at the top:

mod units; // <-- this line!

fn main() {
    println!("Hello, world!");
}

This will declare the module. Now we can use units::LengthUnit:

mod units;

fn main() {
    println!("Hello, world!");

    let m = units::LengthUnit::LightYear;
    dbg!(m);
}

This is difficult to type down, so we can add a use line to bring LengthUnit into scope:


#![allow(unused)]
fn main() {
use units::LengthUnit;
}

Then we can just do:


#![allow(unused)]
fn main() {
let m = LengthUnit::LightYear;
}

And that’s it! We’re sharing code across files.

We just did a simple enum, but it can be constants, structs, functions, whatever.

Adapting for binaries in src/bin

For the programs that we have inside the src/bin/ folder this does not work as Rust is expecting the modules to also live in src/bin/.

Unless you want to have a messy code where all the files are on this folder, we need a way to tell Rust to read in the parent folder instead.

To fix this, we need to create a file named src/lib.rs (in the parent folder).

In this file we will have only the following lines:


#![allow(unused)]
fn main() {
pub mod units;
pub use units::LengthUnit;
}

This will enable us to use the modules in the whole package.

Let’s create a new binary for testing this. We will call this src/bin/conversor.rs.

Remember to update Cargo.toml to add this binary.

As usual, we need a fn main() to start a new binary:

fn main() {
    // ...
}

Notice how conversor.rs does need a main, but units.rs does not.

Now we can bring to scope what we want. Feel free to choose the line you prefer:


#![allow(unused)]
fn main() {
use learnrust::units;
use learnrust::LengthUnit;
}

In my case, I think I’ll settle with use learnrust::LengthUnit; as it’s more convenient.

use learnrust::LengthUnit;

fn main() {
    let furlong = LengthUnit::Furlong;
    dbg!(furlong);
}

Let’s try cargo run --bin conversor:

$ cargo run --bin conversor
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/conversor`
[src/bin/conversor.rs:5] furlong = Furlong

Yay! This works!

What are those other data types for?

So we talked earlier about Rust data types, and we’re just using a few. Why do they exist? Let’s put them together here:

TypeMinMax
u80255
u16065535
u3204294967296
u6401.84 * 10^19
TypeMinMax
i8-128127
i16-3276832767
i32-21474836482147483647
i64-9.22 * 10^189.22 * 10^18

Examples:

  • u8 is generally used to represent characters in the ASCII table, and also bytes in a file in disk.
  • u16 is used for storing UTF16 characters, which are 2 byte long.
  • u32 can be used to store colors of pixels of an image (Red, Green, Blue and Alpha)

While a single number might be small in comparison to the amount of memory a computer has, some programs are in fact “number crunching programs”, where millions or even billions of numbers need to be processed very fast. When you have so much data, compacting it on the smallest possible representation will save memory and/or disk space.

That is why, for what we’re doing in this guide we don’t care that much and use i64 for everything. All in all these are toy programs. Our toy programs don’t do much and the difference in memory used is roughly zero.

In real life, things are different. Say you want to read a JPG photo from your phone camera and do some process on it, like enhancing the photo. A photo can easily be 16 million pixels, and if for each pixel we use an i64 for each color (Red, Green and Blue), we would be wasting a lot of memory.

A program that plays chess by itself has to consider billions of chess possibilities, in this case it is also important to keep each representation as compact as possible.

On the other hand, there are also the two float types, f32 and f64. The difference between them is not the maximum or minimum number like integers, but the amount of significant digits they can carry. f32 can record 6 digits correctly (and a few extra ones) while f64 can record more than 12.

Floating point numbers are used specially on scientific problems. Depending on the problem at hand an f64 might be required, while in others f32 might be better as it has enough precision, but it is smaller, allowing for much complex simulations.

Graphics cards use a lot of floating point numbers to draw scenes for a game. If the precision is not high enough it might lead to jitter: https://www.youtube.com/watch?v=jsLiDQyyBXk

I hope this helps to understand why these types exist, and why we aren’t using them for now.

Const and Static

So far we saw let and let mut. If it’s not mut, it cannot be changed, right?

Well, we have other two ways of creating variables: const and static.

Both have something in common: They can be used to declare names that can be used across all your program, meaning, that they don’t need to be tied to a particular function or scope.

Programming languages typically had global variables, those that you can use across all functions. But Rust almost doesn’t have any global variable.

One of the biggest pain points for people that already know how to code in another language, and they come to Rust is the expectation to create global variables, and they find it near impossible to do.

Const

First, let’s explain what const does. It stands for “constant”, and this means that the value they contain cannot be changed.

We can do something like:

const NUM_THREADS:u32 = 6;

fn main() {
    for n in 0..NUM_THREADS {
        println!("Creating thread {}...", n + 1);
    }
}

Upon compiling, Rust will basically replace all instances where you used a constant with the actual value:

// const NUM_THREADS:u32 = 6;

fn main() {
    for n in 0..6 {
        println!("Creating thread {}...", n + 1);
    }
}

Also, const must have a type. You can’t do:


#![allow(unused)]
fn main() {
const NUM_THREADS = 6;
}

We must do:


#![allow(unused)]
fn main() {
const NUM_THREADS:u32 = 6;
}

If you have a complex struct, it will recreate the struct in every place. For these uses, static will be better suited.


#![allow(unused)]
fn main() {
const ADMIN: User = User {
    id: 1,
    active: true,
    posts: 0,
    // ... 50 more fields ...
}
}

But constants must be values that can be obtained at compile time. Values that require running code at runtime will not work as constants:


#![allow(unused)]
fn main() {
fn calc_pi() -> f64 {
    // ...
    return pi;
}

const PI: f64 = calc_pi(); // <- this doesn't work!
}

Some values surprisingly need to be computed at runtime. For example, vectors.


#![allow(unused)]
fn main() {
const LUCKY_NUMBERS: Vec<i64> = vec![1,6,32];
}

That’s because it really equates to:


#![allow(unused)]
fn main() {
const LUCKY_NUMBERS: Vec<i64> = {
    let mut v: Vec<i64> = Vec::new();
    v.push(1);
    v.push(6);
    v.push(32);
};
}

You can do arrays:


#![allow(unused)]
fn main() {
const LUCKY_NUMBERS: [i64; 3] = [1,6,32];
}

But again, this will create the array each time you use the name.

It might be a bit puzzling, but you can create a constant text if it’s a &str, but not if it is a String.

Or maybe it’s easy to understand that this is a constant value:


#![allow(unused)]
fn main() {
"Hello world"
}

But this is not:


#![allow(unused)]
fn main() {
"Hello world".to_owned()
}

It needs a function call, therefore it can’t be executed in compile time.

Const functions

For the sake of understanding I want to just mention that there exists something called constant functions.

Instead of:


#![allow(unused)]
fn main() {
fn function_name() {
}

They have the const keyword:


#![allow(unused)]
fn main() {
const fn function_name() {
}

These functions can be executed at compile time, and will produce const values.

The Rust standard library has a few of those and can be used to create new constant values.

However, they are very limited. Almost every function can’t be const because they do things that Rust can’t execute at compile time.

I won’t teach in this book how to create these functions, but we can use already existing ones to create constant values.

Static

While const could be created inside a function or globally in the file, static can also be created inside a function or globally.

Static variables are full-blown variables like let. They live in your computer RAM, and they have a memory address.

However, the values they can be initialized to are mostly the same as for const.

The value must be able to be obtained at compile time, for example, as a result of a const function.

Global variables can be modified at runtime in other languages. But not in Rust.

Confusingly, Rust does allow you to have a static mut, but it doesn’t allow you to change it.

statuc mut PAGE_HITS: i32 = 0;

fn main() {
    PAGE_HITS += 1; // error!
}

This is because changing a static variable is unsafe. It can be done by using an unsafe block:

statuc mut PAGE_HITS: i32 = 0;

fn main() {
    unsafe {
        PAGE_HITS += 1;
    }
}

While this does work, this is very bad practice. If you use unsafe, you must know what you’re doing. And turns out that even experienced programmers don’t really know what problems might arise from doing these things.

So the best thing to do is to avoid unsafe altogether and don’t write into static.

Global variables are a symptom of a bad design. If you find yourself trying to do a mutable static, think how to change your program to convert it into regular local variables.

There are other, safe ways, of having mutable statics. We will look onto them later on.

Not everything are numbers and texts: Structs

So far we have seen the basic building blocks for almost everything. Functions, conditions (if), loops (for) and some data types. This will get us already very far.

But! For today’s standards, we need a bit more knowledge. One of the pieces that we have missing and is still critical are structs.

Do not worry. They’re very easy to understand and natural. Structs are basically composite types. This is, a data type that contains different kinds of data.

For what would we need such a strange thing? Those programmers… are crazy. We love making up complicated stuff. Hah! Got you! You were thinking this, right?

Anyway, trust me they’re super useful. Let’s assume we want to track points in 2D space. A point in 2D has two coordinates: \(x, y\)

For this, we could write something like:


#![allow(unused)]
fn main() {
let x = 0.0;
let y = 0.0;
}

And we’re done. No need for structs.

Okay, but we want 5 points. Now what?


#![allow(unused)]
fn main() {
let point1_x = 0.0;
let point1_y = 0.0;

let point2_x = 0.0;
let point2_y = 0.0;

let point3_x = 0.0;
let point3_y = 0.0;

let point4_x = 0.0;
let point4_y = 0.0;

let point5_x = 0.0;
let point5_y = 0.0;
}

Done! Super easy.

But now let’s say we want a function that accepts a point and moves it a certain amount:


#![allow(unused)]
fn main() {
fn move_point(mut px: f64, mut py:f64, dx:f64, dy:f64) -> (f64, f64) {
    px += dx;
    py += dy;
    return (px, py);
}
}

Note that this function returns two values. We haven’t seen this yet.

And now we want to move a point a few units:


#![allow(unused)]
fn main() {
let (point1_x, point1_y) = move_point(5.0, 0.0, point1_x, point1_y);
}

Still feels good this kind of coding? Doesn’t it feel clunky?

Wait! There was a mistake in the above code. We passed 5.0 as the distance where we should have passed the point. The arguments are in the wrong order!

This, my friend, wouldn’t have occurred with clever uses of structs.

Same idea, but with structs:

struct Point2D {
    x: f64,
    y: f64,
}

struct Vector2D {
    dx: f64,
    dy: f64,
}

fn move_point(mut point: Point2D, distance: Vector2D) -> Point2D {
    point.x += distance.dx;
    point.y += distance.dy;
    return point;
}

fn main() {
    let mut p1 = Point2D{x: 0.0,y: 0.0};
    let p2 = Point2D{x: 3.0,y: 0.0};

    let dist = Vector2D{dx: 5.0, dy: 0.0}
    p1 = move_point(p1, dist);
}

Now we’re talking. Wait, too much? Ok, let’s break it down.

First, we define a new data type called Point2D, which is a struct:


#![allow(unused)]
fn main() {
struct Point2D {
    x: f64,
    y: f64,
}
}

It contains two members: x and y. Both are f64 (number with decimals).

With this, we can already create our values of type Point2D.

So, if we did before:


#![allow(unused)]
fn main() {
let p: f64;
}

We can now do:


#![allow(unused)]
fn main() {
let p: Point2D;
}

Because that’s now a user type, and it acts similar to all other types.

To construct it, we simply specify the values of the contents:


#![allow(unused)]
fn main() {
let p2 = Point2D{x: 3.0,y: 0.0};
}

That creates a point p2 that sits at 3,0.

To access the contents, just use the dot operator:


#![allow(unused)]
fn main() {
println!("x: {}, y: {}", p2.x, p2.y);
}

To change the value, you can use the same trick with the dot:


#![allow(unused)]
fn main() {
let mut p1 = Point2D{x: 0.0,y: 0.0};

p1.x = 10.0;

println!("x: {}, y: {}", p1.x, p1.y);
}

Or, if you want, you can also replace the point as a whole instead:


#![allow(unused)]
fn main() {
let mut p1 = Point2D{x: 0.0,y: 0.0};
println!("x: {}, y: {}", p1.x, p1.y);

p1 = Point2D{x: 20.0,y: 0.0};

println!("x: {}, y: {}", p1.x, p1.y);
}

I also did the same for something called vector:


#![allow(unused)]
fn main() {
struct Vector2D {
    dx: f64,
    dy: f64,
}
}

A vector can be used to specify distances or relative positions. While point is absolute. Other than that, it’s the same thing. Works exactly the same.

So why? If they’re effectively the same thing; both have \(x, y\) and use the same type, why bother on creating another one? Couldn’t we use the same Point2D for everything?

Of course! That would work. But it would work… too much.

The issue is that they aren’t technically the same thing. A point is like a position in GPS (30ºN, 20ºW), while a vector is a distance + length (200 km north-east).

If we confuse those two, we can end with:

  • A position that basically is on the North Pole. (At the zero position)
  • A vector that says: travel 300,000 km south.

Both of which are very wrong.

Remember what happened on the function before where we confused both parts? Well, not gonna happen now:


#![allow(unused)]
fn main() {
fn move_point(point: Point2D, distance: Vector2D) -> Point2D {
    point.x += distance.dx;
    point.y += distance.dy;
    return point;
}
}

This function accepts first a point, then a vector. If you accidentally reverse them, Rust is not going to accept the program and error out.

So we have now two types that are almost the same thing but can’t be intermixed.

This is very useful. The amount of errors that get caught this way is tremendous.

Using only one type (point) for everything would at least protect us from mixing accidentally \(x\) and \(y\) coordinates.

But using two types, ensures that the code stays error-free (mostly).

Structs not only are useful to enforce type-safety. They also are needed to get more complex types working.

For example, in an invoicing application you might want to store items.

So we could do:


#![allow(unused)]

fn main() {
struct Item {
    name: String,
    price: f64,
    provider: String,
    observations: String,
    obsolete: bool,
    stock: f64,
    min_stock: f64,
    max_stock: f64,
}
}

And I think this looks very convenient to use, instead of simple variables.

Struct-specific functions, really?

Really. Remember this function?


#![allow(unused)]
fn main() {
fn move_point(mut point: Point2D, distance: Vector2D) -> Point2D {
    point.x += distance.dx;
    point.y += distance.dy;
    return point;
}
}

Turns out that it acts on points, and it only makes sense on points. So we can make this function be part of the struct itself.

The idea is that, instead of writing:


#![allow(unused)]
fn main() {
p1 = move_point(p1, d1);
}

We could write:


#![allow(unused)]
fn main() {
p1 = p1.move_point(d1);
}

Which looks way nicer. And because “point” is implied, we can rename it:


#![allow(unused)]
fn main() {
p1 = p1.move(d1);
}

And it’s way more natural now.

So, how we make this happen?

Implementing the struct. There’s the keyword impl and we just do:


#![allow(unused)]
fn main() {
impl Point2D {
    // Stuff here..
}
}

And inside, we put the function:


#![allow(unused)]
fn main() {
impl Point2D {
    fn move_point(mut point: Point2D, distance: Vector2D) -> Point2D {
        point.x += distance.dx;
        point.y += distance.dy;
        return point;
    }
}
}

The first parameter, we call it Self to make the p.move_point magic happen:


#![allow(unused)]
fn main() {
impl Point2D {
    fn move_point(point: Self, distance: Vector2D) -> Point2D {
        point.x += distance.dx;
        point.y += distance.dy;
        return point;
    }
}
}

Now rename the function to move, because “point” is implied, and voilà:


#![allow(unused)]
fn main() {
impl Point2D {
    fn move(point: Self, distance: Vector2D) -> Point2D {
        point.x += distance.dx;
        point.y += distance.dy;
        return point;
    }
}
}

This is all we need to get the fancy stuff in. And the cool part is that these functions don’t pollute our other code. They’re encapsulated in the type.

If you want to move a point, or do something to a point, you look into the functions that are implemented for the point. Which makes sense, right?

Basically we look, and ask: what do points do? And the list of functions tells us the capabilities of this new type.

Also, if we want we can also call this function explicitly:


#![allow(unused)]
fn main() {
p = p.move(d1);

p = Point2D::move(p, d1); // Exactly the same as the previous line.
}

Why would we want to do this? Well, right now we don’t. It’s longer, uglier, and harder to read. When possible, we will use the shorter form.

But this is the key: when possible. Sometimes is not possible, or very inconvenient. That’s when this form will come in handy.

A bit of keywords

When a struct has a variable inside, it is called a member.


#![allow(unused)]
fn main() {
struct Point {
    x: f64,  // <-- this is a member of Point
}
}

When a function exists inside the implementation of a struct, it is called a method:


#![allow(unused)]
fn main() {
impl Point {
    fn move(mut p: Self, d: f64) {  // <-- this is a method of Point
        p.x += d;
        return p;
    }
}
}

Now you know. Be sure to use these terms as much as possible to avoid those weird looks from senior devs that seem that want to kill someone. “Have you heard? They just called a method a ‘function’… A function! How silly is that?”

Also be sure to ask for a pay rise because of this too.1


1

For those that haven’t caught on the joke, almost all developers are very snob towards naming things. And they act like these have nothing to do, completely different. Furthermore, we keep crafting new names for the same thing, up to the point where the same concept has like 20 names. But it feels so special! I wonder what would happen when they ran out of English words to use2.

2

Wait, I do know. See TMTLA.

OOP? More like OP.

Have you heard of OOP? In the 90s, Object-Oriented Programming was quite a thing. Everyone wanted OOP, more OOP. OOP everywhere.

Well, guess where are we now… everything is OOP.

OOP, OOP everywhere

To the point that a “Hello World” program in Java requires prior OOP knowledge to understand it. It is a bit sad and funny at the same time.

Thankfully Rust hasn’t followed this trend, and we managed to stay out of OOP (for the most part) until now.

But we just saw Structs and methods. And Sir/Madam, this is already OOP.

Java people will tell you (and me) that Rust doesn’t have OOP, and definitely, what I described here is not OOP by a long, long shot. Never, ever!

Whatever. It is Object-Oriented Programming.

The basics of OOP is that you have something called “Objects” (structs or class), and you can spawn many of them. An object has members (variables) and methods (functions) associated with them.

That, you already learned.

There’s lots more to OOP, like inheritance (which Rust doesn’t have), access permissions, interfaces, etc. Depends on who defines it, they will say OOP must have more or less things.

But if you ask me, OOP are structs, members, and methods. That’s it. Everything else is language-dependent.1

You’ll learn later on about Rust Traits, that are similar to Java interfaces. Also, quite a huge thing in OOP.

Bottom line is that you know already the basics, don’t let the OOP herd tell you otherwise.

1

And they will say I have no idea about OOP, coding or whatever. And I’m fine with that.

Need to write back: &mut and *

We saw functions, and to get the data back we were returning values:


#![allow(unused)]
fn main() {
fn test() -> f64 {
}

Sometimes, we want more than one value to be returned. We can use tuples:


#![allow(unused)]
fn main() {
fn test() -> (f64, f64, f64) {
}

This way multiple values can be returned, as we saw before.

But every time we’re creating the values. Sometimes, we want to write on the original variable.

As you’re starting to learn how to code, I would recommend creating new variables each time, and maybe use shadowing.

But if this is definitely a bad idea for your use case, we can instead write into a variable that was passed.

Consider the following code:

fn add_one(mut n: i64) {
    n += 1;
}

fn main() {
    let x = 5;
    add_one(x);
    println!("{}", x);
}

This does not add one to x. In fact, x value will be copied into n inside the function, then it will add one there, and n will be discarded.

x is still 5 and untouched.

It doesn’t matter if x is marked as mut or not. Is the value that is copied, and it is the copy that gets modified, not the original.

Functions provide isolation. They cannot touch the outer variables easily. This is a feature, not a bug.

But what if we want to modify x inside the function? Is it possible?

Yes, but the trick resides in, instead of copying the value, copy the address where the value resides.

Let’s see how this looks:

fn add_one(n: &mut i64) {
    *n += 1;
}

fn main() {
    let mut x = 5;
    add_one(&mut x);
    println!("{}", x);
}

Here, the clue is in the &mut that appears both in add_one declaration, and in the call to that function.

The & operator in Rust borrows the variable. Usually for read-only, but as this is a &mut, it will borrow it for write.

In C++, the & operator retrieves the memory address. It is very similar, if not identical to what Rust does. But Rust semantics are borrowing, while in C++ means literally to get the address.

This changes the type, from i64 to &mut i64. This is important, as they are very different types, even if they look similar. i64 is a number, where &mut i64 is a mutable borrow of a number.

The variable n no longer contains a number, but contains which variable we borrowed.


#![allow(unused)]
fn main() {
x = 5;
n = &mut x;
// n contains a pointer to x: ->x
}

This is a bit confusing, and for now we will cover only the minimum amount to get a basic understanding.

For example, this is wrong:


#![allow(unused)]
fn main() {
fn add_one(n: &mut i64) {
    n += 1;
}
}

Wrong, because n += 1 is not possible here. The variable n is not a number. It points to another variable. Trying to add \(1\) means that we want to change the variable that is pointing at. And that doesn’t make any sense.

To make this work, we need to convert n from the type &mut i64 back into i64.

To do this, we use the * (asterisk) operator. Also known as dereference operator. This undoes the & operator.


#![allow(unused)]
fn main() {
   *n += 1;
}

At this stage I would say it is too early to start using this, but it’s good that you know that this exists and more or less how it’s done.

In Rust, the main reason to do this instead of returning the value is when we have complex objects like Structs, and it can be quite a deal to return a full copy. Other times it is just for performance reasons, as for long strings or buffers it might be better by using a supplied buffer to write instead of creating it each time.

Finally, in some specialized codebases, like programs that must run at real time, we need to ensure that everything runs without waits. And allocating or freeing memory has the potential to induce a wait on the operating system.

As said. This is quite special. I don’t recommend this.

Enums!

We just saw structs, where we could have a type that contains many things at a single time. Structs are one of the basic blocks of programming. Enums are probably their other half.

Consider the following, what if instead of storing many things at a time, we wanted to store one of several options?

For example, a type that could be either an integer or a string. But not both.

The usefulness of this, as usual, is hard to see initially. But examples will follow soon!

A form with options

Something that is easy to relate are real-world forms. Imagine you were trying to get the bank to lend money to you, and they lend you a form.

In the form, the following appears:

What is your current status?

  • (_) Are you working?
    • Name of the company [_______]
    • Field / Type of company [______]
    • Type of contract? () Permanent full time () Permanent part-time (_) Contractor
  • (_) Are you studying?
    • Field of study [______]
    • Years in school [__]
  • (_) Other, not studying or working.
    • Please specify [________]

Or this:

Marital Status:

  • (_) Single
  • (_) Married
  • (_) Other

These “single choice” options can be represented as Enums.

For example:


#![allow(unused)]
fn main() {
enum MaritalStatus {
    Single,
    Married,
    Other,
}
}

Then in code, we can choose one of the three options:


#![allow(unused)]
fn main() {
let status1 = MaritalStatus::Single;
let status2 = MaritalStatus::Married;
let status3 = MaritalStatus::Other;
}

Of course, you can put as many options as you want!

But, for “Other” we don’t really know what happened here, so we want to have the user to specify what they meant by “Other”:

Marital Status:

  • (_) Single
  • (_) Married
  • (X) Other [_________________]

In Rust, we do:


#![allow(unused)]
fn main() {
enum MaritalStatus {
    Single,
    Married,
    Other(String),
}
}

Now, if we specify “Other” we need to put text with it:


#![allow(unused)]
fn main() {
let status3 = MaritalStatus::Other("divorced".to_string());
}

You can also have names and multiple values inside too:


#![allow(unused)]
fn main() {
enum MaritalStatus {
    Single,
    Married,
    Other{ status: String, observations: String},
}
}

#![allow(unused)]
fn main() {
let status3 = MaritalStatus::Other {
    status: "divorced".to_string(),
    observations: "in 2004".to_string(),
};
}

More complex enums are possible, as you can compose them with structs:


#![allow(unused)]
fn main() {
enum ContractType {
    PermFullTime,
    PermPartTime,
    Contractor,
}

struct Work {
    company_name: String,
    field: String,
    contract_type: ContractType,
}

struct Study {
    field: String,
    years: i64,
}

enum CurrentStatus {
    Work,
    Study,
    Other { specify: String },
}
}

This reflects the earlier form:

What is your current status?

  • (_) Are you working?
    • Name of the company [_______]
    • Field / Type of company [______]
    • Type of contract? () Permanent full time () Permanent part-time (_) Contractor
  • (_) Are you studying?
    • Field of study [______]
    • Years in school [__]
  • (_) Other, not studying or working.
    • Please specify [________]

Enums in other languages

In C++, Java and most other languages, enums are much simpler and can’t do most of what Rust can.

In fact, they’re just a fancy way of creating constant values associated to numbers.

Imagine we’re writing a library to open files, and we have a file mode:


#![allow(unused)]
fn main() {
enum FileMode {
    Read,       // -> 0
    Write,      // -> 1
    ReadWrite,  // -> 2
    Create,     // -> 3
    Append,     // -> 4
}
}

These are very similar to creating constants associated to numbers:


#![allow(unused)]
fn main() {
const READ: u16      = 0;
const WRITE: u16     = 1;
const READWRITE: u16 = 2;
const CREATE: u16    = 3;
const APPEND: u16    = 4;
}

Having an enum makes the creation simpler, and groups everything together nicely.

It starts counting at zero, but we can assign a particular number if we want. It will continue counting from there:


#![allow(unused)]
fn main() {
enum FileMode {
    Read = 12,  // -> 12
    Write,      // -> 13
    ReadWrite,  // -> 14
    Create,     // -> 15
    Append,     // -> 16
}
}

If we have two numbers or more assigned, it works too:


#![allow(unused)]
fn main() {
enum FileMode {
    Read = 12,  // -> 12
    Write,      // -> 13
    ReadWrite,  // -> 14
    Create = 20,// -> 20
    Append,     // -> 21
}
}

In Rust, you can extract the actual number by casting to integer:


#![allow(unused)]
fn main() {
dbg!(FileMode::ReadWrite as u16);
}

Another example:


#![allow(unused)]
fn main() {
enum Numbers {
    One = 1,        // ->  1
    Two,            //     2
    Three,          //     3
    Four,           //     4

    FourtyTwo = 42, // -> 42
    FourtyThree,    //    43
}
}

Other examples

Imagine a datatype called When that could take any of the following:

  • “Tomorrow”: As in, the meeting will be tomorrow.
  • 12 (hours): Even will happen in 12 hours.

In a struct, the problem is that we must have two fields and one must remain empty:


#![allow(unused)]
fn main() {
struct When {
    name String,
    hours i64,
}
}

However, Rust doesn’t allow them to be empty. So we might be compelled to use zero as empty or the empty name as not set. But this isn’t a good practice. Using special values with special meanings is a bad idea in programming. We had a long history with special values, and it usually ends in bugs.

Here’s where an Enum helps:


#![allow(unused)]
fn main() {
enum When {
    name(String),
    hours(i64),
}
}

This can only take one of the values. Either is name or is hours.

We use them like this:


#![allow(unused)]
fn main() {
let tomorrow = When::name("tomorrow".to_string());
let hours_3 = When::hours(3);
}

C++ has something similar to this, called union. But it’s not safe to use. Rust does have union as well and trust me, you don’t want to use unions unless you really, really… really know what you’re doing.

They also are useful to define names, for example a list of operation modes:


#![allow(unused)]
fn main() {
enum OperationMode {
    Read,
    ReadWrite,
    Append,
    Create,
    SelfDestruct,
}
}

Don’t confuse Rust enums with C++/Java enums. In these languages enums are useful only to associate a number to a name. Rust enums are way more powerful.

You might say: “Well, nice, but I don’t see myself using this ever. How is this a critical thing to know for a beginner?”

And the answer is that Rust itself is plagued with two very popular enums. You can’t avoid it. You’ll have enums in your code, want it or not.

These enums are Option<T> and Result<T>. I’ll discuss them now.

Option

Option is basically:


#![allow(unused)]
fn main() {
enum Option {
    Some(value),
    None,
}
}

And it is used when a value can be empty. But truly empty. Missing. Nil. Gone.

Because, you see, this is the empty string:


#![allow(unused)]
fn main() {
let text = "".to_string();
}

But it’s not missing. It’s an empty string. The value is not empty; it contains an empty string. Weird, hah!

You want an empty value?


#![allow(unused)]
fn main() {
let text: Option<String> = None;
}

This one is empty.

And we can use this to have optional parameters in a function or a struct:


#![allow(unused)]
fn main() {
struct User {
    username String,
    real_name Option<String>,
}
}

Result

The other enum Result, is similar to this:


#![allow(unused)]
fn main() {
enum Result {
    Ok(value),
    Err(error),
}
}

This is used to have fallible operations. If something fails, it will return an Err(error). The error inside contains details on what failed. If it works, returns Ok(value) where value contains the data we needed.

For example, consider a function to divide:


#![allow(unused)]
fn main() {
fn divide(a: f64, b: f64) -> f64 {
    return a / b;
}
}

However, divide by zero is an error. To be able to communicate this error we can do:


#![allow(unused)]
fn main() {
fn divide(a: f64, b: f64) -> Result<f64> {
    if b == 0 {
        return Err(DivideByZeroError);
    }
    return Ok(a / b);
}
}

Unwrap

As commented, Rust libraries are full of Option and Result. So it’s very easy to find something that returns these things.

For example, if we want to parse a string into a number:


#![allow(unused)]
fn main() {
let num: i64 = "1235".parse();
}

This doesn’t work because parse() will return a Result and not an i64:


#![allow(unused)]
fn main() {
let num: Result<i64> = "1235".parse();
}

But this is inconvenient. If we know that the result is going to be Ok, we can use unwrap:


#![allow(unused)]
fn main() {
let result_num: Result<i64> = "1235".parse();
let num: i64 = result_num.unwrap();
}

Or, in one line:


#![allow(unused)]
fn main() {
let num: i64 = "1235".parse().unwrap();
}

The problem with unwrap() is that will make the program crash if the result is an error. So be careful when using this in places that are not guaranteed to do as expected.

If you are okay with making the program crash at that point, consider using expect() instead, which does the same but allows you to provide a message:


#![allow(unused)]
fn main() {
let num: i64 = "1235".parse().expect("failed to parse credit card number");
}

However, if making the program crash is not a good idea, you have to handle the error:


#![allow(unused)]
fn main() {
let result_num: Result<i64> = "1235".parse();
match result_num {
    Ok(num) => {
        // .. handle here the parsed number ..
    },
    Err(e) => {
        // .. handle the error ..
    }
}
}

Rust forces you to choose what to do with the errors. There’s no “default action” but instead, compiler errors and warnings until you decide to unwrap or handle it. If you don’t decide, you’ll get complaints.

Oh, and this works with option too!


#![allow(unused)]
fn main() {
// TODO: divide was returning a result - find another example!
let result_num: Option<f64> = divide(3.0, 1.2);
match result_num {
    Some(num) => {
        // .. handle here the parsed number ..
    },
    None => {
        // .. 
    }
}
}

Understanding Rust Enums internals

WARN: Technical info ahead! This describes how things work in memory internally. This section is not required to understand, and feel free to skip. But for some readers, this might give some insight and understanding on Enums. Don’t obsess into understanding everything; just a general overview here is fine.

In C++ (and Rust) we have something called “unions”. A union is a type where all contents will be stored in the same place in memory.


#![allow(unused)]
fn main() {
union MyData {
    integer: i64,
    float: f32,
    text: [char; 20],
}
}

These contents will be put one on top of the other, overlapping the same region in memory.

If we did a struct like that:


#![allow(unused)]
fn main() {
struct MyData {
    integer: i64,
    float: f32,
    text: [char; 20],
}
}

In the memory we will have:

  integer float        text
[________][____][____________________]

All variables will be packed one after another.

However, if we were only going to use one of those at a time, we would be wasting a lot of memory of the computer.

Unions instead put everything in the same place:

[________] integer
[____] float
[____________________] text

Or, more accurately:

float integer text
[____]___]___________]

There are bytes in memory that will be shared across the float, the integer, and the text. Because of this, they use only the memory needed to hold the largest variable that they can contain.

This makes unions very dangerous as if you write text and then read float you’ll get back basically garbage.

That’s why, in Rust, reading unions is unsafe.

But if we knew what field we wrote, then we could actually read it without risk of getting back garbage.

Imagine we had constants to specify which field it is:


#![allow(unused)]
fn main() {
const FIELD_FLOAT: u8 = 0;
const FIELD_INTEGER: u8 = 1;
const FIELD_TEXT: u8 = 2;
}

And then we store this along with the union, inside a struct:


#![allow(unused)]
fn main() {
union MyData {
    integer: i64,
    float: f32,
    text: [char; 20],
}

struct MyDataSafe {
    field_written: u8,
    data: MyData,
}
}

Now, as long as we always keep the field_written up to date, we know which one was used, so we can read confidently data without risk of getting back corrupted values.

We could write an implementation like this to ensure this is the case:


#![allow(unused)]
fn main() {
impl MyDataSafe {
    pub fn write_integer(&mut self, i: i64) {
        self.field_written = FIELD_INTEGER;
        self.data.integer = i;
    }
    pub fn write_float(&mut self, f: f32) {
        self.field_written = FIELD_FLOAT;
        self.data.float = f;
    }
    pub fn write_text(&mut self, t: [char; 20]) {
        self.field_written = FIELD_TEXT;
        self.data.text = t;
    }
}
}

And we could read “safely”:


#![allow(unused)]
fn main() {
impl MyDataSafe {
    pub fn read_integer(&self) -> i64 {
        if self.field_written != FIELD_INTEGER {
            panic!("wrong field type");
        }
        unsafe {self.data.integer}
    }
    pub fn write_float(&self) -> f32 {
        if self.field_written != FIELD_FLOAT {
            panic!("wrong field type");
        }
        unsafe {self.data.float}
    }
    pub fn write_text(&self) -> [char; 20] {
        if self.field_written != FIELD_TEXT {
            panic!("wrong field type");
        }
        unsafe {self.data.text}
    }
}
}

Notice two things here. First, when we created the struct:


#![allow(unused)]
fn main() {
struct MyDataSafe {
    field_written: u8,
    data: MyData,
}
}

The layout in memory is:

   float integer text
[_][____]___]___________]
 ^    
 --- field_written

Second, those constants:


#![allow(unused)]
fn main() {
const FIELD_FLOAT: u8 = 0;
const FIELD_INTEGER: u8 = 1;
const FIELD_TEXT: u8 = 2;
}

Are actually a regular C++ enum:


#![allow(unused)]
fn main() {
enum Field {
    Float,
    Integer,
    Text,
}
}

And the struct:


#![allow(unused)]
fn main() {
struct MyDataSafe {
    field_written: u8,
    data: MyData,
}
}

Is actually an equivalent of a Rust Enum!


#![allow(unused)]
fn main() {
enum Field {
    Float(f32),
    Integer(i64),
    Text([char; 20]),
}
}

That’s what it actually is! Rust enums are kind of “safe C++ style unions”.

They use only the memory needed for the biggest value possible of all options, and they’re safe. Plus an extra byte or two to hold which variant are we talking about.

In some cases, Rust is “too smart” and it’s able to omit the extra byte needed to store the variant by using some tricks when compiling. But that is outside what I want to cover here.

Match!

Have you ever seen code like this?


#![allow(unused)]

fn main() {
let choice = 1;

if choice == 1 {
    guess_game();
} else if choice == 2 {
    convert_units();
} else if choice == 3 {
    test_unwrap();
} else if choice == 4 {
    quit();
}
}

Have you wondered if that could be written in a clearer way?

Then you’re in luck! Presenting the match:


#![allow(unused)]
fn main() {
let choice = 1;
match choice {
    1 => guess_game(),
    2 => convert_units(),
    3 => test_unwrap(),
    4 => quit(),
    _ => {},
}
}

As you can see, match has a beautiful syntax. Concise, yet powerful, that goes to the point.

What match does is exploring different options that a value can take. So, if we do:


#![allow(unused)]
fn main() {
let choice: i64 = 1;
match choice {
    // ...
}
}

It is going to consider what values choice can take. Since it is an i64, it can take billions of different numbers, either positive or negative, and for each one we can specify what to do in each case.

But it does have a catch, as match requires us to consider all possibilities. That is, we need to cover all possible numbers and specify an action for it.

To avoid specifying all options one by one, we can have a default by using the underscore _ => ...:


#![allow(unused)]
fn main() {
let choice: i64 = 1;
match choice {
    1 => do_something(),
    _ => panic!("Invalid option!"),
}
}

It can understand ranges too:


#![allow(unused)]
fn main() {
let choice: i64 = 1;
match choice {
    1 => do_something(),
    2..=10 => panic!("This option is not supported yet"),
    _ => panic!("Invalid option!"),
}
}

And it can understand enums too!


#![allow(unused)]
fn main() {
enum Choice {
    GuessGame,
    ConvertUnits,
    TestUnwrap,
    Quit,
    InvalidChoice,
}

let choice = Choice::GuessGame;
match choice {
    Choice::GuessGame => guess_game(),
    Choice::ConvertUnits => convert_units(),
    Choice::TestUnwrap => test_unwrap(),
    Choice::Quit => quit(),
    Choice::InvalidChoice => {},
}
}

And this is useful to handle errors:


#![allow(unused)]
fn main() {
let input_number = "12345"; // provided by user
let res_number:Result<i64> = input_number.parse();

match res_number {
    Ok(number) => println!("your number is {}", number),
    Err(e) => println!("There was an error parsing your number: {:?}", e),
}
}

A bit more advanced usage could be:


#![allow(unused)]
fn main() {
let input_number = "12345"; // provided by user
let number:i64 = match input_number.parse() {
    Ok(number) => number,
    Err(e) => {
        println!("There was an error parsing your number: {:?}", e);
        return;
    }
};

println!("your number is {}", number);
}

Floating point gotchas

Let’s begin saying that, in math:

\[ 1.1 + 1.1 + 1.1 = 3.3 \]

That is obvious.

But in computers:


#![allow(unused)]
fn main() {
let a = 1.1;
let b = 3.3;

let c = a + a + a;

println!("b = {}", b);
println!("c = {}", c);
println!("b == c? -> {}", b == c);
println!("b - c = {}", b - c);

}

b is not equal to c. The difference is -0.0000000000000004440892098500626.

Of course, this is wrong. The difference should have been zero. But this is not how computers work.

Surprised? We thought computers were stupid glorified calculators, but they can’t even get a simple sum right.

This is because how decimals work in a computer when using floating points. They use base 2 (ones and zeros) to represent the numbers, and when we have a number that has a decimal point, this becomes a problem.

If you’re not interested on the explanation, just remember that you cannot expect any two floats to be equal. You have to check if they are “close enough”. Also, before converting them to integer, you first have to round them. You might think you have 295.0 but in reality it might be 294.999999999996, and if you just cut the decimals instead of rounding you get 294 instead of 295. It feels stupid, because it is.

We need to understand that not all numbers can be written down.

Yes, let that sink in. There are numbers you can’t write.

For example \(\pi\), we can write 3.1415 but doesn’t matter for how long we try, it will never be the real \(\pi\). Because it’s irrational.

But on the real numbers it also happens. Even in the rational ones (fractional).

A common example is \(10 / 3\):

\[\frac{10}{3}=3.333333333…\]

It doesn’t matter how long you keep writing, you cannot write it down.

Why?

Well, our system is decimal, base 10. And \(10 = 2 × 5\).

This means we can only represent fractions that divide by multiples of 2 and 5.

So, for example the following fractions are representable:

\[\frac{1}{25}\]

\[\frac{6}{2560}\]

But \(1/3\) is not, because 3 is a prime, and our decimal system doesn’t include 3 as a factor.

Now, for a computer that is using a binary system the only factor is 2.

This means that when a number is the result of dividing by 5, we can probably represent, but the computer cannot.

The number 1.1 is in reality:

\[\frac{11}{2 × 5}\]

The computer can represent \(11/2=5.5\) but not \(11/10=1.1\).

That will lead to infinite decimals, but a f64 will cut at some point. And there you have your imprecision. While your written number comes with full precision, the one written by the computer can’t get that level of precision regardless of having 20 or 500 decimal places.

Do you think it’s lame?

Try writing down:

\[\frac{100}{29}\] \[\frac{100}{53}\] \[\frac{100}{97}\]

Were you able to do it? No?

Lame!

Well, that’s how your computer feels. Please be respectful. They only have base 2 to work with.

Is this a Rust problem?

No, it is a hardware problem. All languages suffer from this. Some of them are better than others at hiding this fact.

There exist “Decimal” implementations in some languages including Rust that use base 10 and get rid of this problem entirely.

But those are much slower, but used in financial applications to prevent errors.

Why this wasn’t addressed?

When they created floating points, computers were very slow and memory was very limited. Adding support for base 10 decimal numbers would have been near impossible and most of the programs of the era wouldn’t have enough resources to run.

Also, floats were invented for scientific purposes, like sending people to the moon. In this case, high precision was better than having base 10. Because even if they can’t fully represent some numbers, overall they give better precision to numbers that arise from physics calculations.

So they went with that, and we’re stuck with them forever. Nowadays, the majority of the applications would benefit from base 10, and there is more than enough resources to run them in this manner. Yet we keep doing this in software.

I guess that everyone got used to this, and no one cares anymore.

Conclusion

Comparing if two floats are equal: bad.

Comparing if two floats are not equal: very bad.

Converting to integer without rounding: super-mega bad.

What to do instead?

Comparing if two floats are equal:

Instead, check if the difference between them is very small:


#![allow(unused)]
fn main() {
let a = 1.1;
let b = 1.1;

if (a-b).abs() < 0.00000001 {
    // equal
}
}

Comparing if two floats are not equal:

Instead, check if the difference between them is bigger than a very small number:


#![allow(unused)]
fn main() {
let a = 1.1;
let b = 1.1;

if (a-b).abs() > 0.00000001 {
    // not equal
}
}

Converting to integer without rounding:

Please round first:


#![allow(unused)]
fn main() {
let a:f32 = 294.999999999;
let b:i64 = a.round() as i64;
}

Other problems

Not A Number?

See 0/0.

Infinity?

See 1/0 and -1/0.

Positive and negative zeros?

See \(1 × 0\) and \(-1 × 0\)

Not sortable

In Rust, if you have data in a table that are floats, and you want to sort this data, you can’t.

Because of NaN, floats cannot be sorted. But they can be partially sorted.

Not infinitely big

Loss of precision at very big numbers

Project: A simple game with ggez

Now that we know a bit about structs, enums and similar stuff, we’re ready for our next game.

As expected, we will change the game engine to something more appropriate: ggez

https://crates.io/crates/ggez

In comparison with Macroquad that we have seen before, this one uses structs. Because of this we can organize our code much better and make a game that can grow in features without getting too convoluted.

Space-recycler

A horizontal shooter game (ship) where the goal is to avoid asteroids while collecting as much trash as possible.

For extra difficulty, we’ll have different recycling bins and the player has to put each item in the right bin. Because this is a game about recycling!

This is a complex project, and it will take quite a bit of time to complete. It took me around 20 hours to figure out everything, so I guess for someone that is still learning it could take 1-2 weeks.

But no worries, I will be assisting, so hopefully that cuts back as much time as possible.

Creating the new project

As this is a complete game, we will have it in its own crate (or folder). We’ll begin by creating it:

$ cargo new spacerecycler

We can add already the dependencies we will need in Cargo.toml:

[package]
name = "spacerecycler"
version = "0.1.0"
edition = "2021"

[dependencies]
ggez = "0.7.0"
rand = "0.8"

[profile.dev.package."*"]
opt-level = 2

That would add ggez at version 0.7 and rand at 0.8.

The part for [profile.dev.package."*"] is important. That tells Rust how to compile the dependencies when in debug mode. opt-level = 2 enables the optimizer at the same level as for release.

Effectively this makes a debug build to have the libraries compiled as optimized as possible. And it’s important because ggez and all the dependencies that it pulls are related to graphics. And graphics are intensive operations. While developing, we will need those to be as fast as possible.

However, our code will be still compiled fully in debug mode. This means that compilation times will be fast, and it will detect most of the errors at runtime properly.

For now, proceed to build it. This will take a long time so make a cup of tea:

$ cargo build

This will download a lot of libraries and compile them. Because of the amount of stuff that needs to be built, expect 10 minutes of compilation time.

Remember, these dependencies will be built in almost release mode. Building in release takes more time.

Once it is built, we can go ahead and start some coding.

The first window

The setup phase is a bit large, so please trust me for a minute.

On main.rs copy the following contents:

mod game;

use ggez::conf::{NumSamples, WindowMode, WindowSetup};
use ggez::event;
use ggez::ContextBuilder;

use game::SpaceRecyclerGame;

const WIDTH: f32 = 1200.0;
const HEIGHT: f32 = 600.0;
const MARGIN_W: f32 = 15.0;

fn main() {
    let window_cfg = WindowSetup {
        title: "Space Recycler".to_owned(),
        vsync: false,
        samples: NumSamples::Four,
        ..Default::default()
    };
    let window_mode = WindowMode {
        width: WIDTH,
        height: HEIGHT,
        maximized: false,
        resizable: false,
        visible: true,
        resize_on_scale_factor_change: false,
        ..Default::default()
    };

    // Make a Context.
    let (mut ctx, event_loop) = ContextBuilder::new("space_recycler", "Cool Game Author")
        .window_setup(window_cfg)
        .window_mode(window_mode)
        .build()
        .expect("aieee, could not create ggez context!");

    // Create an instance of your event handler.
    // Usually, you should provide it with the Context object to
    // use when setting your game up.
    let space_recycler = SpaceRecyclerGame::new(&mut ctx).expect("Error intializing game");

    // Run!
    event::run(ctx, event_loop, space_recycler);
}

We need to create another file game.rs with the following:


#![allow(unused)]
fn main() {
struct SpaceRecyclerGame {
    // Your state here...
}

impl SpaceRecyclerGame {
    pub fn new(_ctx: &mut Context) -> SpaceRecyclerGame {
        // Load/create resources such as images here.
        MyGame {
            // ...
        }
    }
}

impl EventHandler for SpaceRecyclerGame {
    fn update(&mut self, _ctx: &mut Context) -> GameResult<()> {
        // Update code here...
        Ok(())
    }

    fn draw(&mut self, ctx: &mut Context) -> GameResult<()> {
        graphics::clear(ctx, Color::BLACK);
        // Draw code here...
        graphics::present(ctx)
    }
}
}

If we execute cargo run now, that should present us with a black screen.

The compilation time now it should be quite fast.

There’s a lot here. Let’s dissect step by step.

Level: Master

Well, well… here we are.

Look what you’ve done so far. Those programs look quite like the real deal!

Yet, the journey continues. Now it’s time to get in-depth to what coding is.

With this part, you should be ready to get a job in programming or contribute in the Rust community.

Borrowing

This is most probably the most difficult concept in Rust. Borrowing is very specific to Rust only, and it’s the main reason so many people abandon learning Rust. And also it is why so many people say Rust is difficult.

Learning this is what sets apart developers from Rust Developers.

Are you ready?

  • When you see things like x, y, f64, etc. It means that you own the variable, meaning that you can do whatever: read, write and free their memory.
  • If you see &x, &y, &f64, etc. Means that you don’t own it, it was lent to you temporarily. You can only read their contents, but most of the time you can also copy the contents elsewhere too.
  • Finally, if you see &mut x, &mut y, &mut f64, etc. Is as before, but you can also write to them. With these you can do mostly everything except free their memory.

That’s it. & is read-only access, &mut is read and write access, and if nothing appears, it’s full access.

Wait, you thought this was going to be more difficult? I wonder why.

Ok, jokes aside, now comes the difficult part.

Rust will ensure the correctness of our programs in a way that feels “too much”, it will analyze and prove correctness to a level that no other language that I know does.

This makes the Rust compiler very picky. There’s lots of stuff that it doesn’t like. And it’s hard to make the compiler happy.

Maybe you experienced this already. Or maybe I did a good job up to now keeping you away from this. Now it’s time to start introducing (very gently) to Ownership and Borrowing in Rust.

When we store a value into a variable, it uses some amount of memory of the computer. It seems kinda obvious that 100 numbers will use more memory than 5 numbers.

And memory needs to be freed. Because if we keep asking for memory and never returning it, our computer might run out of memory at some point.

Therefore, Rust must allocate (ask for memory) when we put a new variable, and it must free the memory when it’s no longer needed.

fn some_function() {
    let x: i64 = 12345; // <-- memory gets allocated here.
    println!("using the variable: {}", x);
} // <-- memory gets freed here, when the function ends.

fn main() {
    some_function();
}

Now, this is the basic of what Ownership means: when we create something, we own it, and we’re responsible for freeing it when we’re done with it.

Rust does this for us in a very natural way. So much that we could ignore this up to this point.

The problem with this approach is that, when we pass a variable to a function, the function now owns that variable. And this means that the function will free this variable when it ends, we could not use it anymore.

Sounds weird, but it will seem clear here:

fn print(x: i64) { // <-- x ownership is captured when calling the function.
    println!("Value is: {}", x);
} // <-- memory gets freed here, when the function ends.

fn some_function() {
    let x: i64 = 12345; // <-- memory gets allocated here.
    print(x); // <-- we call the function, but 'x' is now owned by 'print'.
    println!("using the variable: {}", x); // <-- 'x' no longer exists!!
}

fn main() {
    some_function();
}

But —I hear you asking—, if I run the above code it works.

Yeah, I know. But this one does not:

fn print(x: String) { // <-- x ownership is captured when calling the function.
    println!("Value is: {}", x);
} // <-- memory gets freed here, when the function ends.

fn some_function() {
    let x: String = "text".to_string(); // <-- memory gets allocated here.
    print(x); // <-- we call the function, but 'x' is now owned by 'print'.
    println!("using the variable: {}", x); // <-- 'x' no longer exists!!
}

fn main() {
    some_function();
}

Some data types are so simple that Rust can just copy them around. But text in the other hand, is a bit more complicated and that trick no longer works1.

Now, to make this work is pretty easy. Because print() is only reading, we only need read-only access. We can use the ampersand to borrow temporarily the variable:

fn print(x: &String) { // <-- we only get a "reference" to 'x'. We don't own it.
    println!("Value is: {}", x);
} 

fn some_function() {
    let x: String = "text".to_string(); // <-- memory gets allocated here.
    print(&x); // <-- now we only lent 'x'. We keep ownership.
    println!("using the variable: {}", x); // <-- 'x' does exist here and works.
} // <-- 'x' memory gets freed here.

fn main() {
    some_function();
}

As you can see, I only added two & in the code, and we don’t have the problem anymore.

Now we also have &mut, and this one is fascinating. It allows us to do this:

fn add_five(x: &mut i64) { 
    x += 5
} 

fn some_function() {
    let x: i64 = 100; 
    println!("x = {}", x); 
    add_five(&mut x); 
    println!("x = {}", x); 
    add_five(&mut x); 
    println!("x = {}", x); 
}

fn main() {
    some_function();
}

Because add_five receives a mutable reference, when we change it, we will see the changes appear on some_function.

I think you can see this is super useful, and our functions got now superpowers!

Let’s step this up a little.

Turns out you can also store references like &x or &mut x. And for what reason do we want to do this? Well… it’s too early to explain. Let’s just say that it is possible, and you don’t want to do this.


#![allow(unused)]
fn main() {
let mut x = 5;
let y = &mut x;
*y = 3;
println!("x = {}", x);
}

Also in structs:


#![allow(unused)]
fn main() {
struct Thingy {
    x: &mut i64,
    y: &f64,
}
}

But this must be written as:


#![allow(unused)]
fn main() {
struct Thingy<'a> {
    x: &'a mut i64,
    y: &'a f64,
}
}

The point is: it is possible. And I need you to know that this can be done to understand the next explanation.

But at your current level, if you see yourself coding something like that, simply back off and find a way to avoid storing references &var &mut var. They’re very painful to work with.

Now, this means that there may exist several pieces of code that have access to the same variable at the same time via different methods.

And this is a problem.

If one part of a program is using a variable and another part, inadvertently is changing the variable… well, it can end very badly.

For this reason, Rust limits how ownership and references can coexist, and how many we can have.

  • Ownership (var): There can be only one owner at a time. When we call a function, we transfer the ownership of the variables unless they are references &x &mut x.
  • Mutable reference (&mut var): There must be at most one &mut var at any time. And while it exists, the original variable cannot be read or written (more or less).
  • Shared reference (&var): There can be many, but they cannot coexist with &mut var.

In other words, there must be always one owner, and one writer. Readers can be many, as long as there aren’t anyone writing.

The ownership system in Rust can be related with having a car:

  • var: You own a car, therefore you can choose to dispose of it at any time.
  • &mut var: You send the car to a mechanic, they can do changes to your car, but you cannot use it meanwhile. And you cannot dispose of it while is on the mechanic.
  • &var: You let others see your parked car, but not touch it. Many can see your car at the same time, but meanwhile you cannot use it or send it to the mechanic.

Which roughly translates into:

struct Car {
    horses: i64,
    air_conditioner: bool,
}

fn buy_cheap_car() -> Car {
    return Car{horses: 60, air_conditioner: false};
}

impl Car {
    // "self" here transfers ownership. And this function will free the variable.
    fn dispose(self) {
        println!("Bye car! {} horses, A/C:{}", self.horses, self.air_conditioner);
    }
    // "&mut self" gives exclusive write access.
    fn upgrade_ac(&mut self) {
        self.air_conditioner = true;
    }
    fn upgrade_engine(&mut self) {
        self.horses += 10;
    }
    // "&self" gives shared read access.
    fn admire(&self) {
        println!(
            "Woo, nice car with {} horses and {} A/C", 
            self.horses, 
            self.air_conditioner
            );
    }
}

fn main() {
    let mut mycar = buy_cheap_car();
    let admirer1 = &mycar;
    admirer1.admire();
    
    let mechanic = &mut mycar;
    mechanic.upgrade_ac();

    let admirer1 = &mycar;
    let admirer2 = &mycar;
    // mechanic can't be used here.
    admirer1.admire();
    admirer2.admire();

    let mechanic = &mut mycar;
    mechanic.upgrade_engine();

    let admirer1 = &mycar;
    let admirer2 = &mycar;
    admirer1.admire();
    admirer2.admire();

    mycar.dispose();
    // Car no longer exists, this won't work:
    // let admirer1 = &mycar;
    // admirer1.admire();
}

I know that at this point this will feel confusing. It’s a lot to unpack.

And I guess you have questions like:

  • Why should I use this?
  • Seems complicated.
  • Why not use &mut all the time? Or just the regular var.
  • Is this to put restrictions on the code?

No, no… forget about all those thoughts.

It’s not something we want to use, it’s something that we will need to use. Meaning, that the usage will be apparent soon enough. Don’t overthink it.

We’re trying to solve a problem here. The problem is sharing variables across many pieces of code.

Before, we were doing lots of things like:


#![allow(unused)]
fn main() {
let p: Point2D;
// (...)
p = p.move(distance_vector);
}

But this is inconvenient. And we would like to just write:


#![allow(unused)]
fn main() {
let p: Point2D;
// (...)
p.move(distance_vector);
}

And that should update the point.

For that, we want to use &mut self instead. So, we had:


#![allow(unused)]
fn main() {
impl Point2D {
    fn move(mut p: Self, v: Vector2D) { 
        p.x += v.dx;
        p.y += v.dy;
        return p;
    }
}
}

With what we learned, we can do:


#![allow(unused)]
fn main() {
impl Point2D {
    fn move(&mut self, v: Vector2D) { 
        self.x += v.dx;
        self.y += v.dy;
    }
}
}

And now that updates the point. Nice.

Still one problem. If we do:


#![allow(unused)]
fn main() {
let mut p1 = Point2D{x: 10.0, y: -5.0};
let mut p2 = Point2D{x: 5.0, y: 15.0};
let distance_vector = Vector2D{dx: 50.0, dy:0.0};
// (...)
p1.move(distance_vector);
p2.move(distance_vector);
}

The second move, for p2 won’t work. The problem is that move is taking ownership of distance_vector, so we lose the variable.

But we don’t need ownership just for reading the contents, do we?

So let’s update that:

struct Point2D {
    x: f64,
    y: f64,
}

struct Vector2D {
    dx: f64,
    dy: f64,
}

impl Point2D {
    // Added ampersand         v---- here
    fn translate(&mut self, v: &Vector2D) { 
        self.x += v.dx;
        self.y += v.dy;
    }
}

fn main() {
    let mut p1 = Point2D{x: 10.0, y: -5.0};
    let mut p2 = Point2D{x: 5.0, y: 15.0};
    let distance_vector = Vector2D{dx: 50.0, dy:0.0};
    // Added ampersand
    //           v---- here
    p1.translate(&distance_vector);
    p2.translate(&distance_vector);
}

With just that single & character, now it works.

I’ll leave this here. Take it as an introduction into borrowing and ownership. Later on we will revisit this topic a bit more in-depth. So if it’s not fully clear, don’t worry!

Also, we’re reaching a point where you can already read (and probably write) what could be production-ready Rust code. This is (almost) what real code looks like. You’re getting close to be able to start your own applications!


1

If you ever asked yourself why all code samples and exercises I wrote almost always had number variables, this is the reason. They’re easier to work with and avoids these problems.

Level: Grand Master

Last steps! This section is what makes a programmer a great one!

Will go through the most complex parts of coding and see more challenging programs.

What is a Garbage Collector?

It’s something that Rust doesn’t have. Have a good day!

Garbage Collector is about memory allocation. In C & C++ memory needs to be manually requested and freed. Because this is very dangerous, most other languages automatically allocate and free.

Allocating is “easy”, as the first use is allocating. But automatic free is hard, because you need to ensure that no parts of the program can access that variable anymore.

So a Garbage Collector is a system that can track when variables go out of scope (unreachable) in order to free the memory. There are lots of types of GCs (each language might use a different approach), but they tend to have performance penalties, memory over-consumption and may temporarily halt all threads of the program to do the free.

  • In Python, the GC prevents Python from running several threads of python code in parallel. So most python code, even if it uses threads, effectively is single-threaded because of the GC.
  • In Java, the GC tends to free memory very late, causing bloat, a lot of memory consumption.
  • In Go, the GC causes micro-pauses. All threads suddenly stop for a few milliseconds to clean up.
  • In Rust, there’s no GC. Instead, the automatic free is computed at compile time (by static analysis). So no bloat, no performance penalty.
  • C & C++, there’s no GC, you do it manually, and a mistake causes memory corruption and segfaults.

Rust is kind of a compiler assisted C++. Automatic like Python, but if the compiler cannot prove your program is correct (even if the program in fact IS correct), it will reject it and won’t compile.

So the drawback from Rust approach is the added difficulty of learning Ownership & Borrowing (which I talk about in my blog post) ….

Level: Legendary

Wait, do you want more?

Maybe you already surpassed the teacher.

Appendices

Appendix A. License of this book

This text is licensed under the Creative Commons license “Attribution 4.0 International (CC BY 4.0)” which can be read here:

https://creativecommons.org/licenses/by/4.0/

https://creativecommons.org/licenses/by/4.0/legalcode

Appendix B. Caveats and challenges in this book

This book is intended to people with no previous experience in coding at all and attempts to teach programming in a very approachable way.

Because of this, a lot of information is hidden, misrepresented, or factually incorrect. It is needed to hide and oversimplify stuff, so new learners don’t get overloaded with information. This also helps the reader to create their own programs very early on without needing to have a proper understanding, which can come in later on.

Other books might put a lot of theory in front to give a good understanding of what is coming next, but theory is indeed boring until it’s put into use. In this book I try to push down the theory as far as possible.

If this approach is good or bad it’s certainly subjective. But as everyone else goes for a more academic way, and there is almost no one that does something like I do in this book, I think it’s good to add more choices for learning.

Appendix C. Exercises and Solutions

Appendix D. Proposed projects

Some ideas to build as a beginner!

  • Unit convertor - changes cm to m, yards to miles, °C to °F, etc.
  • Expense tracker / Lending tracker (see how much you owe to your friends and settle)
  • Horizontal Shooter game (maybe about cleaning space and recycling)

Appendix E. Rust compared to other languages

  • Rust has structs and impl.

    • C++, Java, Python, JavaScript have classes.
    • C doesn’t have classes, but it has structs.
      • However, no function methods can be placed.
  • Rust has generics

    • C++ and Java have it too.
    • C and Python do not.
      • Python doesn’t need it.
    • Go is adding them.
  • Rust has traits

    • Java and Go have interfaces.

Features unique to Rust

  • The fact that by default variables are not mutable. In other languages you have to specify “const” to get the opposite effect.

  • Move by default instead of Copy.

  • Ownership and Borrowing.

  • No Garbage Collector and no manual memory management (At the same time)

    • C/C++ have no GC, but manual memory allocation.
    • Almost every other language has a GC.