What is your favorite programming language?
If your answer isn’t Rust then please read this post. At the end of it all, your answer might have changed!
In this post, I will teach you the basics of Rust programming. Enough to get you started and create your own programs.
At the end of the article, I will guide you in some very nice directions for further studies.
You can return to this article during your Rust training as a small dictionary or lookup article.
Learning a new programming language is like learning any other language. The fastest way is to try it and in this case, you don’t need a friend from a different country or an aircraft. You just need your new friend - the Rust compiler. And believe me, it will be a very good friend indeed.
So feel free to code along and try out examples of your own or simply read this with a cop of strong coffee like you would any other article or book.
Let’s begin…
Table of Contents
· Installation and First Project · Variables · Data Types ∘ Numbers ∘ Booleans ∘ Strings, &str and Characters · Collections ∘ Array ∘ Tuple ∘ Vector ∘ Hash Map · Functions · Control Flow and [Loops](#95ce) ∘ Conditions ∘ Loops · Ownership and Borrowing · Structs, Enums and Methods · Error Handling · Tests · Advance Concepts: Generic Types, Traits, Lifetimes, Closures and Concurrency
Rust is here to stay and if you want to learn a performant and modern language that is both safe and future-proof, Rust is a good choice.
Rust is a systems programming language that runs blazingly fast, prevents almost all crashes, and eliminates data races.
Even though Rust is classified as a systems programming language, you can really build anything in Rust. You just need to sacrifice some time. In return, you get safety and mind-blowing speed.
Rust has gotten a reputation of being hard to learn, but I think that it really comes down to the initial approach and mindset.
You just need to think a little differently when coding – like a Rustacean!
Installation and First Project
The installation of Rust is painless thanks to a little nifty cli called rustup.
You can install Rust by following the instructions here or by just Googling.
When you have installed Rust and its fantastic package manager called Cargo, you are good to go.
Say you want to create a project called "rusty_cli".
Find a location on your file system where you want to place your project. Then "cd" yourself to that location in the cmd/terminal.
When you are inside the folder that you want to place the project, simply use cargo new rusty_cli
.
Cargo will work its magic and create a project structure that it understands, including a folder called src
, and inside will be a file called main.rs
.
The file main.rs
is the entry point of the program. In order to run the program, you have a few different options using Cargo.
You can use cargo run
to run the main file in debugging mode or cargo build
to create an executable. This executable will be stored in target/debug
.
You don’t have to use cargo to compile and run Rust programs, but it is a very friendly tool and I highly recommend using it.
When you are ready to build the final executable, you can use the command cargo build --release
to compile it with optimizations. This command will create an executable inside target/release
instead of target/debug
.
The optimizations make your Rust code run much much faster but with a small penalty in compilation time.
Variables
Variables in Rust are defined using the let
keyword and are immutable by default. Let’s have an example. The following code will not compile just yet.
If you are following along, then you’ll see the compiler complaining about something like "cannot assign twice to immutable variable".
In order to fix this, we could do the following:
Note that if we put the keyword mut
in front of the variable name then the variable becomes mutable.
Also, note how expressions work in Rust. An expression is something that returns a value. If you put a semicolon you are suppressing the result of this expression, which in most cases is what you want.
This will be clearer when we get to functions in a little while.
Data Types
Data types usually go hand in hand with arithmetic operations, but I won’t dedicate much space for this since the usual operations on numbers apply in Rust as well.
Many of the data types in Rust are shared with other languages. We have integer types such as i32
or i64
and floating-point numbers as for instance f64
.
We also have the data type bool
which can contain the values true
or false
, we have string
_, &str
_and char
for storing text data and tuple
, array
, vector
and hash map
as some of the common collections.
On top of this, we have structs
, methods
and associated functions
as a replacement of classes and methods in OOP languages and an interesting type called trait
which is kinda like an interface in e.g. Go.
Besides structures and tuple structs for structuring related data, we also have the type enum
which plays a central role in the Rust language.
The usage of some of these types is best learned through examples and by experimenting on your own, however, I think some initial explanations and examples at this point are in order.
Numbers
Note that when you declare variables, you can choose to specify the data type by explicitly stating it in the declaration.
If we want to store a variable containing an unsigned integer of size 8 bits, we have to do something like the following:
If you had just written let age = 18;
then Rust would have inferred the type of age to be of typei32
.
Booleans
Booleans in Rust called "bools" are like the booleans in other languages. The syntax is true
and false
(note the lower case) and an example could be let b = true;
.
Strings, &str and Characters
The character type is written with single quotes like ‘c’ and the keyword denoting the data type is char
.
Strings are a bit more complicated in Rust than they are in e.g. Python but that is because a lot of the details are kept hidden in Python. Well, not so much in Rust!
The reason for this is to prevent future errors when dealing with strings. A common concept in Rust is that the language goes a long way to ensure safety by dealing with possible errors at compile time. This is, by all means, a good thing, but it puts some demands on the programmer.
Strings are implemented as a collection of bytes, together with some methods to provide useful functionality when those bytes are interpreted as text.
Strings are more complicated than many programmers give them credit for. That is because they look natural to the programmer – after all, we modern humans are used to dealing with text in everyday life.
However, since there is a difference between what we would consider being a meaningful substring and how the machine would interpret substrings, things are not as easy as they seem. We need something called encodings as a bridge between the machine and our brain.
We won’t go into details about the world of encodings but if you find yourself unable to sleep one night, then by all means take a research tour and dive deep into encodings, it is more fascinating than you might think!
Rust encodes strings in UTF-8
and unlike in Python, we can make strings growable in Rust.
As indicated above Rust has more than one kind of string. The type String
and the type str
, where the latter is usually seen in the form &str
, and is __ called a string slice or _string litera_l.
A String
is stored as a vector of bytes Vec<u8>
and comes with convenient methods:
We will deal with vectors shortly. For now, you can think of them as a typical array.
Exactly what the difference is between the two types of strings will be skipped for now, but what I can say is that if you make a variable like the following:
let s = "Towards Data Science";
then s will be of type &str
. If you want to store "Towards Data Science"
as a String type, you would either do
let s = "Towards Data Science".to_string();
or equivalently:
let s = String::from("Towards Data Science");
The main difference between the two types is that &str
is immutable and stored on the stack and String types can be made mutable and are thus stored on the heap.
If you don’t know about the stack and the heap, don’t sweat it. The most important thing to know about them is that accessing and writing to the stack is much faster than the heap.
Data stored on the stack needs to be of a fixed size during the whole runtime. This means that if a variable is allowed to grow in size, then it needs to be stored on the heap.
The two types are also related as indicated by the name string slice by &str
being a substring of a String type, whatever that means (try to find out yourself).
Since this is a short introduction, we will leave this topic and this data type for now but the data type of strings is a rich and exciting topic and I encourage you to take a deep dive into this subject after you are done reading this post.
Collections
In Rust, we have many collections, but some of them are used more than others.
Array
An array in Rust is like an array in Go for example. It is of fixed size and thus can be stored on the stack which means quick access.
In an array, you can only store one data type at a time.
The syntax is like Python’s list:
let rust_array = [1, 2, 3];
and you can access a specific element in the same way as in Python. rust_array[0]
gives us the first element and in this case 1.
Tuple
The tuple type is commonly used in Rust for storing different data types in one collection.
We can access the elements with a dot notation:
Vector
Vectors are one of the most commonly used collections in Rust because like Python‘s lists or Go‘s slices, the vector in Rust can grow in size.
It can only hold a single data type but as you will see later, there are ways around that. We will have to know about a type called enum
to begin writing about that though.
The fact that a vector can grow makes it a very important type as you would imagine.
To create a new, empty vector, we can call the Vec::new
function:
let mut v: Vec<i32> = Vec::new();
Once created, we can now push elements into it. For instance, v.push(1)
will append 1 to v
and the size has now grown.
If you want to instantiate the vector with elements in it, we have a convenient macro for that:
let v = vec![1, 2, 3];
We will see more macros a little later.
There are two ways of getting elements from a vector. We can do it like in Python with the v[i] notation getting the _i_th element in this case. But we need to be careful that the vector actually has enough elements.
If it has fewer elements and we try to access an element that does not exist, then we’ll get an error at runtime! This is dangerous so Rust has another way of accessing elements in a vector.
We access it like let second = v.get(1);
Now second
will be of type Option<T>
which I haven’t covered yet, but soon you’ll know what that means and how to get the value out.
We can also iterate over vectors in a for loop but since I haven’t covered loops yet, I’ll wait with showing you the syntax as well.
Hash Map
Hash maps are like dictionaries in Python or maps in Go and The type is denoted HashMap<K, V>
. It stores a mapping of keys of type K
to values of type V.
To initialize and populate a hash map, we could do the following:
The insert
method pushes data into the hash map by a tuple of key and value as an argument.
If you want to access the data inside the hash map, then we have the get
method for this as well. Specifically,
let a = articles.get(&String::from("Python Graph"));
will return an Option
type we can then get the value from. The symbol &
will be covered shortly as well.
When we want to update the hash map by a key, and we are not sure whether it’s there already, we have several options. One of them is the convenient entry
method:
articles.entry(String::from("Rust")).or_insert("https://www.cantorsparadise.com/the-most-loved-Programming-language-in-the-world-5220475fcc22");
If the key is already in the hash map, then we’ll get a mutable reference to it, if not, then we’ll insert the data specified in the argument to or_insert
. References will be explained in a bit.
Functions
Functions are one of the most important features of Rust.
We have seen the main function which plays a special role, but we are of course able to create other functions as well.
The syntax is simple. Here is an example of another function in Rust:
Note that we need to tell Rust which data type the function accepts as input and which type it will output. This is a good thing since it prevents many errors.
Another interesting thing about Rust is that you don’t have to explicitly tell Rust what you return by a return
keyword. If the last statement in a function has no trailing semicolon, then that is what the function returns.
This also applies to blocks of code wrapped in curly braces in general. A lot like Scala.
In the above example. the function plus_one
returns x+1
where x is the integer that the function takes in as an argument.
In Rust, we have a return
keyword, but it is usually only used if you want to return something early.
Control Flow and Loops
The syntax of control flow operations is usually best learned by playing around with small exercises yourself.
Conditions
Take a look at the following program and try to modify it:
We see that comments in Rust are done by a double slash or alternatively /* blah blah */
for multiline comments. Note also how we are able to return something early with the return
keyword.
The if, else if, else
syntax is like Go‘s. No parentheses are needed around the boolean expression which is great for readability.
Loops
In Rust, we have several different kinds of loops.
If you want an infinite loop, we have the loop
which of course should be broken at some point. Therefore, Rust also has a convenient feature that enables you to break specific loops and not just the inner one.
The following example is borrowed from The Rust Book which we should all have under our pillow (or saved as a bookmark in these times).
Note how we can label the loop by the 'label: loop
syntax and break a specific loop by calling that label based on some conditions later.
We also have a while loop which is quite intuitive to use:
Then of course comes the for loop which has the following syntax:
We are able to loop over iterators and whether you have to make your data type an iterator is of course dependent on the type itself. Note that this syntax is a lot like in Python except you rarely have to change your types in Python.
Ownership and Borrowing
The one feature that really sets Rust apart from other languages is its clever Ownership system for handling memory.
You see, different Programming Languages deal with memory in different ways. In some languages, you have to allocate and free up memory yourself which makes your code fast and gives you low-level control. You get "closer to the metal" so to speak. An example of this could be C.
In other languages, you have what is known as a garbage collector which makes sure that you are safe and clears up memory automatically. This is convenient and safe, but it does not come for free. It slows down the program. Examples of this include Python, Java and Go along with many others.
Rust has taken a completely different approach to memory management. Rust combines safety and speed in a system known as Ownership.
As the Rust book puts it:
Because ownership is a new concept for many programmers, it does take some time to get used to. The good news is that the more experienced you become with Rust and the rules of the ownership system, the more you’ll be able to naturally develop code that is safe and efficient. Keep at it!
First, let’s state the rules of ownership:
- Each value in Rust has a variable that’s called its owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
Interesting… What does it mean?
Consider the following code:
This will actually not compile. To understand this, let’s see it from an ownership perspective.
First, the variable s1
comes into scope by taking ownership over the value String::from("Hi")
. Then the variable s2
takes ownership over that value and we know from the ownership rules that there can only be one owner.
This means that the variable s1
is no longer valid. In Rust we talk about "a value being moved". So what is really happening is that the ownership or value is moved to s2
and therefore we can’t access s1
after that.
If s1
and s2
had been e.g. i32
, then this wouldn’t have happened because their values would have been copied instead. This has to do with how Rust stores different values. If a value is stored on the heap, then these rules apply but if it is stored on the stack and thus implements the Copy
trait, then a simple copy of the value will happen.
Let us take a look at another example where we’ll see that functions can take ownership as well.
The following example is also from the Rust book.
You can clearly see what happens by looking at the comments. When a variable is passed as a parameter to a function, ownership of that variable is moved to the function and the variable becomes inaccessible in the current scope.
Also, note that if a variable stored on the heap like a string or a vector for example goes out of scope, then memory is freed only if ownership hasn’t moved before then.
At this point, I am sure this seems a bit limiting and cumbersome that you can’t even print out the variable without transferring ownership and thus make the variable inaccessible.
Of course, there is a system that addresses this issue.
This system is called _borrowing. M_any other languages have pointers. Rust has references and these play well together with the borrowing rules.
Consider the following code:
This actually compiles and runs without any issues at all. The &
prefix is called a reference and when a function is fed a reference or if a variable is assigned a reference, they don’t take ownership over it.
In the above, we need to use the variable s1
in the println!
macro and therefore we cannot let the function calculate_lenght
take ownership. The function is only borrowing an immutable reference to the value that is assigned to s1
which is what we want.
We can also have mutable references but there are some rules governing this feature to avoid nasty issues like race conditions etc.
The rules of referencing are the following:
- At any given time, you can have either one mutable reference or any number of immutable references.
- References must always be valid.
The way to remember this is that it is like any other file. It is fine to distribute read rights to several different people at a time because nothing bad can happen in that case. However, if you give several people write access to a file at the same time, a lot of issues might occur.
This is what happens when you get a merge conflict in Git right? We shouldn’t have that issue in our code because Rust doesn’t know how to handle this. Therefore, there can be at most one mutable reference at any given time.
Structs, Enums and Methods
Structs and enums are in many ways the core building blocks of Rust. If you come from an object-oriented language, you will feel right at home.
When we introduced hash maps, we created an articles
hash map to store articles. However, what if we wanted to save more information about them such as the publisher or the length in minutes?
Could we make a custom data type that contained such structured information? Well, yes.
Take a look at the following:
At the bottom of the file, we have created a struct called Article
. We are now able to store related data in one single container. The link, magazine and length data are called fields and we are able to access them with the dot notation which we will see in a bit.
Let’s see how to implement methods on structs using the impl
syntax.
The output of this program is the following:
The article "The Most Loved Programming Language in the World" is about 4 minutes long and is published in Cantors's Paradise.
Please go to https://www.cantorsparadise.com/the-most-loved-programming-language-in-the-world-5220475fcc22 to read it.
The article "How to Give Your Python Code a Magic Touch" is about 6 minutes long and is published in Towards Data Science.
Please go to https://towardsdatascience.com/how-to-give-your-python-code-a-magic-touch-c778eeb9ac57 to read it.
An enum is a lot like a struct but in an enum, one can have subtypes of a given data type which can be very useful.
Consider the following changes to out code:
The output from this program is:
The article "How to Give Your Python Code a Magic Touch" is about 6 minutes long and is published in Towards Data Science.
Please go to https://towardsdatascience.com/how-to-give-your-python-code-a-magic-touch-c778eeb9ac57 to read it.
Programming article coming up!
The article "The Most Loved Programming Language in the World" is about 4 minutes long and is published in Cantors's Paradise.
Please go to https://www.cantorsparadise.com/the-most-loved-programming-language-in-the-world-5220475fcc22 to read it.
A couple of new features are introduced here as well as collecting some old ones.
First of all, notice how we have created a Story
enum that contains two variants: Mathematics
and DataScience
. This is convenient because recall in the vector subsection above when we said that the vector can only hold one type? Well, we have created a vector that holds the type Story
but there are variants to this and the variants don’t necessarily have to contain the same type.
Both DataScience
and Mathematics
contain the type Article
but we could have chosen say String
and f64
if we wanted.
Then we also introduced the match
control flow operator which takes an enum in this case and checks its variant. If we get a match, we execute some code.
Pretty simple, but extremely powerful.
Recall that the get-method on both vectors and hash maps returned some type called Option
.
Option
is an enum that has two variants: Some
and None
.
Rust forces you to deal with None
values in a controlled and safe way, unlike other languages.
Notice that we can have nested match clauses. The output from this code will be the same as before but then also a line:
We have less than three elements...
In this way, we can deal with missing values in a safe way.
Error Handling
Rust needs to deal with errors like any other programming language does. There is no such thing as error-free software.
In Rust though, we have enums at our disposal and that means safe error handling!
Sometimes we actually need to crash our program. That may sound weird, but if an error is unrecoverable and dangerous, then it is better to just shut down.
This can be done with the panic!
macro.
The panic!
macro wraps up by clearing the memory before shutting down. This takes a little time, and this behavior can be modified.
Most times we are actually able to handle the error gracefully. This is done with the Result
enum.
We haven’t talked about generic types yet, so this shouldn’t really make sense to you.
Just replace T
with any data type and E
with any Error type.
You don’t need to get all of this as of now. But I think that you get the main point. File::open
returns a Result
enum that may or may not contain an error.
You can use pattern matching to extract and handle it. Once you learn about closures, you’ll have more options at your disposal.
Tests
Tests in Rust are built into the fabric of the language itself. Take a look at the following snippet:
You should add a test module to every file you write. We haven’t talked about modules in Rust but you get the idea.
If you run cargo test
all your tests will automatically be run.
The huge advantage of testing in Rust is the extremely useful compiler. In fact, the best practice in Rust for developing software is by the use of test-driven development (TDD).
This is because the compiler comes with great suggestions for possible solutions to your problems. So much so it almost writes the code for you.
Take a look at this small tutorial to see what I mean:
Advance Concepts: Generic Types, Traits, Lifetimes, Closures and Concurrency
There is much more to learn about Rust but if you followed along this far, you definitely are on the right track.
The features in this headline are what you should focus on next but I will redirect you from here since this is only an introductory article and not a full-blown book.
If you are not a member of Medium yet but would like unlimited access to stories like this one, all you need to do is click the below:
Resources for further reading:
The rust book:
Rust by Example:
Documentation:
Let’s Get Rusty: