Rust for total beginners: computations
Introduction
In our previous post we learned what was the meaning of our code and how to make the computer talk to us. In this post, we will discover how to make it do some computations! There will be a lot of theory to cover but it is necessary to better understand how to program.
Variables
One of the most fundamental concept of programming is variables. Until now, the computer was only printing some text. What about remembering something? Think about it, when using a calculator it is sometime interesting for us to use the last result of a computation. But, to do it, the computer needs a way to store this value somehow. This is done by using variables!
Unlike a calculator, we can have many different variables when we are programming. So the computer needs a way to distinguish between them. And that is why every variable has a name and a value. The name of the variable is used to know which variable we are talking about and the value of the variable is the data we want the computer to record. Here is how to create a variable:
We used a new keyword let to define a variable with name a and value 0. We say that the value 0 is bound to the variable a. Now, the computer knows that if we use the variable a it will instead mean to use the value 0. How can we use it? We can add an argument when calling the println macro. Inside the text (the first argument) we want to print we can use “{}” which means “replace me with the value of the second argument”.
We can have multiple instructions between the one where we create our variable and the one where we use it. The point of using a variable is that the computer will remember it later.
We can choose any name we want for our variable but there are some rules to follow. First, we need to use ASCII characters, we cannot use other characters such as 愛, φ, or à (remember, we can still use them when using the println macro). Second, we have to begin our variable name with a letter. Third, we cannot use blank space, the name has to be “connected”.
Another rule which is more like a convention: we should use snake case style for our variable name. When naming a variable, we may find that a long name will be better than a short one, such as “dog image”. However, how would we write it without the blank space? We could have “DogImage”, “dogImage”, “dogimage”, or even “DoGiMaGe” (please never do the last one). In Rust, it is a convention to use the snake case style which says to put an underscore “_” between the words with lowercase letters. In our case our variable would be “dog_image”. Another example, if we try to compile the following code:
It compiles but the compiler issues a warning:
The compiler is warning us that we are not following the Rust convention. Our program will still work but it should be modified.
Types
Now, let’s talk about the value we want to save. Let say we want the computer to save the number 1 in its memory. We will write some code to do it and after being compiled the computer will remember it. But how? The computer possess many different kind of memory such as the registers, the RAM or the harddrive. When we are asking our computer to record a value it will access to one of these memory places and store the value there. When we will ask for the value later it will go to retrieve it from where it was stored.
Intuitively, storing the number 1 will take few space while trying to save a big image will take more. The computer needs to know the size of the value we want to store to not waste memory (or being short of it). A solution is to use types. A type is like a label on the variable indicating which kind of variable it is. Some variables will be numbers, some other will contain text, some will be complex data combining multiple values. By knowing the type of a variable the computer is able to know exactly how much memory it needs to store it.
In Rust, every variable has a type and the compiler needs to know it when it is compiling. We say that Rust is a statically typed language. However, in the previous examples we did not tell anything to the computer but it still worked. This is because the Rust compiler is a little bit smart and can infer the type of our variable directly by reading our code. But in certain situations it will not be able to infer it and will ask for our help.
Today we will only see variables containing numbers.
Integers type
An integer is an element of such as 0, 1, 42, 789478041 or -419. But remember, after being compiled, our Rust code becomes machine code made of binary “0” and “1”. How does the computer handle numbers if it can only count to 1? The answer is: using binary numbers. A binary number is a number expressed with only two symbols (0 and 1), such as “1001101”. Every symbol/digit is called a bit. The more bits we have, the more numbers we can write in a binary base. If you want a better explanation, you can read more here.
Rust provides many types to store an integer so we need to chose one. First, we need to know if our variable will handle only non-negative integers or if it is possible that both negative and non-negative integer could be stored. A signed binary number will have one bit representing the sign (+ or -) of the number while an unsigned binary number will not. Therefore, for the same number of bits, an unsigned binary number will be able to represent more numbers than its signed equivalent. Then, depending on the value we want to store, the number of bits will change. For example, if we are using an unsigned 8-bits binary number, we can represents the integers from 0 to 255. A signed 64-bits binary number will be able to represents the integers from -2147483648 to 2147483647.
The mathematical formula is simple. If we have an unsigned n-bits binary number, we can store the integers from to . If we have a signed n-bit number, we can store the integers from to . The choice of signed/unsigned and the number of bit will decide which type to use for our variable. Some examples of type names are i8, u8, i32 or u128. The first letter (“u” or “i”) means “unsigned” or “signed integer”, the number shows the number of bits. We can see here all the possible choices for Rust integer types.
So, do we have to check everytime and do the computation for all the values we want to store? No! Usually, we will not have to do anything, the compiler will choose for us. Of course, depending on our program we may want to optimize our code to use the less memory possible and to be as fast as possible. In this situation we could need to use u8 to be really efficient. But for now the compiler choice should be good enough.
Great, now let’s start coding! Our program was:
Like I said before, every variable has a type. So what is the type of a? A common trick to discover it is to write the following:
If we try to compile it, we see:
We tried to assign a new value “()” of type () to the variable a of type integer so the compiler threw an error. This is a quick way to find the type of a variable. So, here the type of a is integer. Wait, wasn’t it supposed to begin with a letter and then a number? Well, yes. However in this case we did not really choose a type for a (we did not write anything), we only bound the value 0 to a. The compiler assumed that it was an integer (of type integer) and, if not more information is given, will choose as default to use the i32 type. However, if later in the code the compiler infers that it is (for example) u128 then it will automaticaly change the integer type into u128.
We do not really need to do it, but how can we tell the compiler that for example we want our variable a to be of type i64? We just need to add a type annotation, like this:
This time if we try to compile it we will received this error:
Hooray! The variable a is now an i64. Now, let’s do some computations!
Manipulating integers
We have seen a lot of theory, now is the time to code! Computers are really good to, well, compute things. One of the most basic operation is to add numbers. In Rust, it is pretty straightforward:
And it works! We could also substract or multiply easily:
In the programming world, the * operator means . By the way, in this example I wrote comments. We can write anything we want if we first write “//”, the compiler will consider that it is a comment until the beginning of a new line. Comments in programming are VERY importants. It is a very good habit to write comments describing what our code is doing because maybe in the future it will be read by other people and they may not understand it without explanations. Worse, it often happens that the same person who wrote the code has to read it later and find himself incapable of understanding it. So please, try to comment your code!
What about division? To divide we use the “/” operator:
And the result is:
That is good. What about ?
Wait, what? It should be 7.5 but the computer gave 7, why? The answer is simple. When we are using the operators +, -, * or / on two integers, the result will be an integer. However, is not an integer, it is a rational number, so the computer cannot give us the correct answer. Instead, it gives us the quotient of the euclidian division. In our case, we can write as 15=2*7+1. 7 is the quotient and 1 is the remainder of the euclidian division.
If we want to get the remainder we can use the symbol “%”:
It seems difficult to use the division with integers because we may result with rational numbers. One way to avoid this is to use floating-point numbers.
Floating-point numbers
Using integers to make computation is really easy for a computer. However, when we begin to want to use rational numbers or real numbers everything becomes more complicated. How would we represent a real number with a non predictible decimal part such as in a computer? Or even a rational number with an infinite decimal part such as $\frac{1}{3}=0.33333333..$? Besides, many problems arise when doing operations. Therefore, scientists all over the world discussed about it and in 1985 a standard was created: the IEEE 754 for Floating-Point Arithmetic (IEEE754). It gave a unified vision on how should computers handle decimal numbers.
Rust gives two types for floating-point numbers: f32 and f64. The numbers in the type names once again mean the number of bits to represent the decimal value. However, as with integers, the compiler is able to infer that we want to use a float (floating-point) type just by reading our code:
If we do not precise it the compiler will choose f64 by default because on modern CPUs it will be almost as fast as f32 but with more precision (more bits). And, again, if we want to force the usage of a particular type we can use type annotation:
Manipulating floats
Floats and integers share the same operators:
But this time the division gives the expected result:
Note that in the code we did not tell the compiler that a and b were floats, yet it infered it was the case because we wrote 15.0 and 2.0. If we just write 15 and 2 the compiler will infer that they are integers.
We can also use the “%” operator (named modulo) to get the remainder of the euclidian division:
What about mixing integers and floats?
The error is very explicit: we “cannot add i32 to f32”. In Rust, we need our numbers to share the same type if we want to use an operator on them. It is not only an “integers vs floats” problem, we will have the same result if we try to add two integers with different types. Try it!
Exercises
From this post I will give some exercices at the end of each post. Programming is a skill that cannot be acquired only by reading, instead it requires you to actively think on how to solve some problems. It also require some curiosity, I highly encourage you to try anything you want to understand the limits of what you can do and what you cannot.
Here are some exercises about variables and types:
1) Try to add an integer and a float. What happens?
2) Can we create a variable c from the result of a and b?
3) The type u8 can only contain integers from 0 to 255. What happens if we try to bound the value 256 to an u8 variable? What if a is an u8 binding the value 1 and b is an u8 binding the value 255, what happens if you try to add a and b?
Conclusion
In this post we discovered how to do some computations on numbers. More precisely, we learned about variables and their types. This post contained a lot of theory but it was important to understand how to program. In the next post we will see a new type representing something else than numbers: booleans. Until now, our program was following all the instructions one by one. We will see how to have instructions that depend on some conditions.