Half a Year of Rust š¦
Rust is a fairly new programming language. For the last half year, I have explored the language and all its unique features. In this blog post, weāll look into five reasons that contribute to making Rust a ā in my opinion ā cool programming language. You will get a high-level introduction to the most important features including the concept of ownership and borrowing. By the end, you will have a better understanding of why Rust is worth exploring and how you can get started with your journey to learn Rust.
fn main() {
println!("Hello, Rust!");
}
Outline
- š Static Typing
- š Concise & Expressive Syntax
- š Performance
- š¾ Concept of Ownership & Memory Safety
- š Detailed Error Messages
š Static Typing
In my opinion, static typing can offer great benefits. Here is an interesting talk on the topic: āWhy Static Typing Came Back ā Richard Feldmanā.
In the past, I was a big fan of Python. I just had the feeling that the short syntax and not needing to specify types enabled me to write code fast. But that was only one side of the story. I probably spent way more time debugging my code, but of course, back then I didnāt count that in.
The main benefit of a static-typed language is that it helps you to spot bugs during development. This means that a lot of bugs wonāt reach production, where your users might see them. Overall you are faster as you spend less time debugging, especially if your project or code base grows in size.
Rust is a statically typed language, which means that you have to specify types for your variables and functions. The Rust compiler will then check if the types and the concrete values match. Based on the concrete values and how you use them, the Rust compiler can also infer types. This is no unique feature of Rust. Static typing has been around for quite some time in a lot of high-level languages like Java, Kotlin, or TypeScript.
Integer Types
let age: u8 = 28;
let degrees_celsius: i32 = -10;
// underscores can be used as visual separators that Rust does not care about
let population = 1_472_000; // same as 1472000
Integers can be signed i
or unsigned u
. Signed integers can be negative. Unsigned integers are always positive. Additionally, the number 8-128
specifies the memory space in bits. So u8
is an unsigned integer that takes up 8 bits of space. There are also theusize
and isize
types that depend on your computer architecture. If you are unsure what type to use, then use thei32
type as the default.
Floating-Point Types
let gpa = 3.9; // type is inferred as f64
let pi: f64 = 3.14;
Rust has f32
and f64
as floating-point types. Use f64
as the default type, since it is roughly the same speed as f32
but more precise.
Boolean Type
let rust_is_awesome: bool = true; // true or false?
Character Type
let c: char = 'a';
let rustacean = 'š¦';
Tuple Type
let mut tuple: (i32, u32, bool) = (-10, 25, true);
tuple.0 = 10; // works
Tuples can be used to group values of different types. Tuples have a fixed length, so once you have defined them, you canāt append or remove values. But you can still change existing values if they are of the same type.
Array Type
let password: [i32; 5] = [1, 2, 3, 4, 5];
let same_numbers = [1; 5]; // same as [1, 1, 1, 1, 1]
Every element of an array must have the same type. Similar to tuples, arrays have a fixed length in Rust. Arrays are saved on the stack in Rust.
Vector Type
let vector: Vec<i32> = vec![1, 2, 3];
let empty_vector: Vec<i32> = Vec::new();
Similar to arrays, vectors can only store values of the same type. Vectors are more flexible and donāt have a fixed length, so you can remove or add values. Vectors live on the heap.
String Types
// String literals
let message = "Hello, Rust!";
// String Collections
let name: String = String::from("Alice");
String Literals are surrounded by double quotes and have a fixed size determined at compile time. String Literals are of the type &str
which is called String Slice. String Slices are immutable and often used as string references.
String Collections are dynamically sized and mutable, meaning that they can change during runtime.
Warning: Strings are not simple in Rust! (You can find out more here)
Option Type
There is no null in Rust
Yes, youāve read that correctly. Oftentimes stated as the billion-dollar mistake of programming, Rust just removed it. Here is what Rust offers instead.
enum Option<T> {
Some(T),
None,
}
Some(T)
: Represents the presence of a value of type T
. The Some
variant contains the actual value.
None
: Represents the absence of a value. The None
variant indicates that there is no value present.
Why is this good? What is the benefit?
- The Option type encodes the common scenario in which a value could be something or nothing
- So why is it better? => Because the compiler will prevent us from using variables when there is no value and we have to explicitly handle the option type
To handle an Option type you can use the match
expression that we will explore in the section on Concise & Expressive Syntax.
Result Type
enum Result<T, E> {
Ok(T),
Err(E),
}
The result type is a built-in construct to handle exceptions or error cases in Rust.
Ok(T)
: Represents a successful operation and contains the value of the operation.
Err(E)
: Represents a failed operation and contains information about the error.
Additional Types
There are multiple additional types in Rust like Structs, Slices, and Closures. We wonāt cover these here as this blog post would have gotten too long otherwise.
š Concise & Expressive Syntax
Rust achieves a nice balance between readability and expressiveness.
Functions
fn add_numbers(a: i32, b: i32) -> i32 {
a + b // same as: return a + b;
}
Rustās function syntax is clear and concise. fn
defines a new function. ->
is followed by the return type. Function parameters and return types need to be explicitly declared. Rust allows the omission of the return
keyword and the ;
symbol to return the last expression in a function implicitly, making the code easier to read.
Making variables explicitly mutable
Rust promotes a safer approach to mutability by requiring an explicit declaration with mut
. With mutability, we tell the compiler that we are planning to change a variable. This improves code clarity, as demonstrated below:
fn main() {
let mut counter = 0; // Explicitly declaring mutability
counter += 1;
}
Match expressions with Option and Result
Rustās match
expression is a powerful and versatile construct, somewhat comparable to a more feature-rich version of switch
statements found in some other languages. You have already learned about the Option and Result type in the section on Static Typing. Rustās match
expression provides a concise way to handle different cases, especially with Option
and Result
types. Consider these examples:
fn process_option(opt: Option<i32>) {
match opt {
Some(value) => println!("Value: {}", value),
None => println!("No value"),
}
}
fn process_result(res: Result<i32, &str>) {
match res {
Ok(value) => println!("Result: {}", value),
Err(error) => println!("Error: {}", error),
}
}
Beyond Option
and Result
, the match
expression can be used with any enumerable type. It's not limited to specific types, making it a flexible and widely applicable tool for pattern matching.
Defining Ranges
Rust provides a concise syntax for defining ranges, particularly useful in for
loops. The ..
syntax creates a range for us. Also, note how readable the loop itself is āfor each number in a range from 1 to 5ā:
fn main() {
for number in 1..=5 { // (1, 2, 3, 4, 5)
println!("Current number: {}", number);
}
}
1..5
would create a range from 1 to 4: (1, 2, 3, 4)
.
Destructuring
Rustās destructuring assignment simplifies working with tuples, arrays, enums, and other types. The example below showcases this feature for tuples:
fn get_coordinates() -> (i32, i32) {
(3, 7)
}
fn main() {
let (x, y) = get_coordinates();
println!("Coordinates: ({}, {})", x, y);
}
š Performance
Rust is for people who crave speed and stability in a language. By speed, we mean both how quickly Rust code can run and the speed at which Rust lets you write programs.
This quote is taken from the book The Rust Programming Language. It highlights that Rust is prioritizing developer productivity without sacrificing runtime performance. Letās take a deeper look at how Rust achieves that.
Zero-Cost Abstractions
Zero-cost abstractions are language features in Rust that allow for a better developer experience without incurring any additional runtime overhead. Which essentially means that you can use simple built-in helpers without making your code slower. One example is the contains
method that works with vectors as well as with arrays:
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let contains_three = numbers.contains(&3);
println!("The vector contains 3: {}", contains_three);
}
The method contains
is an easy way to check the presence of a value in a vector. It is clear what is going on and it is also easy to read that code. Letās look at the implementation of contains
:
pub fn contains(&self, x: &T) -> bool
where
T: PartialEq<T>,
{
self.iter().any(|y| x == y)
}
The contains
method uses the iter()
method, which returns an iterator over the elements of the collection. The any
method is then called on the iterator, which short-circuits as soon as it finds an element equal to the target (x
in this case). The key here is that the implementation leverages Rustās ownership and borrowing system, allowing for iteration without unnecessary copying or runtime overhead. We will have a deeper look in the section on the Concept of Ownership & Memory Safety.
The Compiler
Rust is often embraced for its great performance statistics during runtime. Why did I highlight the runtime fact here? Because the compile performance is still one of the most criticized points about Rust. Just the fact that this is an actual question in the Rust FAQs shows that developers have some concerns: Rust compilation seems slow. Why is that?
One of the reasons for the perceived slow compile time is the code translations and optimizations that the Rust compiler is performing. Another is the Rust type system, that needs to enforce all the constraints that make Rust safe at runtime.
Zero-cost abstractions cost compile time after all
Freedom from Garbage Collection
Rust has no Garbage Collection. Garbage Collection is a feature often used by programming languages to free up memory that is not used anymore. Unfortunately, this can create a performance overhead. After all the Gargabe Collector is just some code that checks if objects are not referenced anymore and then deletes them. As we will see in the next chapter Rust has a different concept of freeing up memory. But most importantly the absence of a Garbage Collector makes Rust more performant.
One prominent example of Rustās performance is Discord switching from Go to Rust. Discordās engineers dived into Rust, enticed by its memory efficiency and absence of a garbage collector, ultimately achieving performance surpassing Go.
š¾ Concept of Ownership & Memory Safety
The concept of ownership is one of Rustās core features. Ownership is also considered one of Rustās most distinctive features, granting it the ability to provide strong memory safety guarantees.
To appreciate the significance of ownership in Rust, itās instructive to contrast it with the memory management practices in other languages. For instance, in Java, memory management often relies on a garbage collector, a mechanism responsible for automatically reclaiming memory that is no longer in use. Rust, however, takes a different approach. It manages memory through a system of ownership with a set of rules checked by the compiler at compile time. This ownership system is so efficient that none of its features introduce runtime overhead, ensuring that your Rust program remains performant even as it adheres to strict memory safety rules.
Ownership Rules
- 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.
Scope
The concept of ownership revolves around the notion of scope, determining when variables come into existence and when they stop to exist (they go out of scope). Rust ensures memory safety by enforcing strict rules on ownership, preventing issues like dangling pointers and data races. When a variable goes out of scope, Rust calls a special function for us, known as drop
. Consider the following code snippet:
fn main() {
let x = String::from("Hello, Rust!"); // x comes into scope
// Some operations with x
} // x goes out of scope, and Rust automatically calls the drop function
In this example, the variable x
comes into scope when declared and goes out of scope at the end of the block, ensuring proper memory management. Rustās automatic invocation of the drop
function at the closing bracket ensures that the memory is freed.
References and Borrowing
Rustās approach to handling references and borrowing is fundamental to its ownership model. Instead of transferring ownership, Rust allows the borrowing of variables through references. This ensures that multiple parts of code can access data without compromising safety. For instance:
fn main() {
let my_string = String::from("Rust is powerful!");
print_length(&my_string); // Passing a reference to my_string
}
fn print_length(s: &String) { // & denotes that we receive a borrowed value
println!("Length: {}", s.len());
} // s goes out of scope, but no ownership is transferred
In this example, the print_length
function borrows a reference to my_string
without taking ownership, demonstrating Rust's ability to handle shared access to data securely.
Memory and Allocation
Rust provides precise control over memory allocation, preventing common pitfalls like memory leaks. The ownership system ensures that memory is deallocated when variables go out of scope. Consider this example:
fn main() {
let my_string = String::from("Hello, Rust!");
// 'my_string' ownership is moved to 'string_owner'
let string_owner = my_string;
// The following line would result in a compilation error since
// 'my_string' is no longer valid
// println!("Original string: {}", my_string);
// 'string_owner' is still valid
println!("Transferred string: {}", string_owner);
}
In this example, the ownership of the my_string
variable is moved to string_owner
. Attempting to access my_string
after the ownership transfer would lead to a compilation error, highlighting Rust's stringent memory management.
Additionally, Rust allows dynamic memory allocation using the Box
type for creating values on the heap. This combination of ownership, borrowing, and controlled allocation empowers developers to build efficient and safe systems.
Here is a blog post, from one of my colleagues that goes into more detail about the concept of Ownership:
š Detailed Error Messages
As developers, we all have experienced how frustrating cryptic error messages can be. Sometimes you just donāt know what to do or where to look. You copy and paste the error message into StackOverflow or Google and still get no results.
In languages like Java or C++, deciphering error messages can feel like navigating a labyrinth of technical jargon. Just consider these examples:
#include <iostream>
#include <vector>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
int sum = 0;
for (auto it = numbers.begin(); it != numbers.end(); ++it) {
sum += numbers.erase(it); // Intending to remove each element
}
std::cout << "Sum: " << sum << std::endl;
return 0;
}
The noble intention here is to calculate the sum of elements as they are erased from the vector. However, the C++ compiler, in its wisdom, graces the developer with an error message that reads like an ancient prophecy:
error C2662: 'void std::vector<int,std::allocator<_Ty>>::erase(const std::
vector<int,std::allocator<_Ty>>::iterator &)': cannot convert 'this' pointer
from 'const std::vector<int,std::allocator<_Ty>>' to 'std::vector<int,std::
allocator<_Ty>> &'
This confusing error message makes debugging feel like a challenging puzzle. Youāre lost without clear clues on where to look, what exactly went wrong, or even how to fix it.
In Rust, the compiler is your best friend.
Rust is just awesome for its descriptive and concise error messages. The compilerās error messages are designed to guide you through understanding and resolving issues. Oftentimes you even get hints about how to solve your errors. Letās look at an example that we already used and see what happens if we comment out one line:
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let contains_three = numbers.contains(&3);
// println!("The vector contains 3: {}", contains_three);
}
warning: unused variable: `contains_three`
--> src/main.rs:3:9
|
3 | let contains_three = numbers.contains(&3);
| ^^^^^^^^^^^^^^ help: if this is intentional,
| prefix it with an underscore: `_contains_three`
We do not use the variable contains_three
anymore. The Rust compiler immediately warns us about this and states it clearly unused variable
. Also, the compiler tries to figure out what we want to do and gives some help on how to fix the issue (prefixing the variable with an underscore). Consider another example:
fn main() {
let greeting = String::from("Hello");
let message = greeting;
println!("{}", greeting);
}
error[E0382]: borrow of moved value: `greeting`
--> src\main.rs:4:20
|
2 | let message = greeting;
| -------- value moved here
3 | println!("{}", greeting);
| ^^^^^^^^^ value borrowed here after move
|
= note: move occurs because `greeting` has type `String`,
which does not implement the `Copy` trait
This message not only indicates the error but also educates us on the concept of ownership and borrowing, fostering a deeper understanding of one of Rustās core principles. We are not allowed to use a value after it was moved to ensure memory safety.
In essence, the error messages just make learning a breeze, by providing actionable insights and enhancing our comprehension. You donāt have to worry about getting something wrong the first time, as the compiler is your friend and will help you learn to write clean Rust code. The detailed error messages are one example of Rustās commitment to developer-friendly design. There even is a course that builds on this concept of learning Rust by debugging code!
ā¤ļø Bonus: Programmers Love It
The Most Loved languages are those that appeal to veteran developers.
For the last couple of years, Rust has been one of the most loved languages by seasoned software developers according to Stack Overflowās Developer Survey.
However, a nuanced analysis of data reveals interesting trends. Despite its popularity among experienced developers, Rust is not extensively used by those learning to code, highlighting a gap between love and learning. As weāve seen, Rustās detailed error messages play a role in its reputation as a developer-friendly language, offering clear guidance for learners. But I wouldnāt recommend anyone new to programming to start with learning Rust. Itās no surprise that on Stack Overflow, the languages favored by learners, like JavaScript and Python, continue to lead in the total number of tags. If that does not scare you to explore Rust further then read my guide on How to Start Learning Rust below.
Conclusion
Rust brings a unique set of features to the table, making it an excellent choice for developers seeking performance, reliability, and memory safety. Its ownership and borrowing system ensures memory safety without sacrificing performance, and its detailed error messages aid in debugging and learning. Rustās intuitive syntax and static typing make code easier to read, write, and maintain. These are just some of the points why many programmers love the language.
How to Start Learning Rust
Learning Rust can be difficult. Especially in the beginning.
I can agree with this chart. It is from this YouTube video How to Learn Rust ā No Boilerplate, which is also a great resource to get started. Here is my guide that was inspired by that video:
- Read the book āThe Rust Programming Languageā from cover to cover once. You can read it for free online.
2. Read the book again. But this time use the interactive version from Brown University and do the exercises.
3. Read āRust By Exampleā
4. Do the rustlings course
Looking for a Job?
This blog post was made possible by Comsystoās concept of Lab Time where employees can use part of their working hours to explore new technologies and build what excites them the most. If you want to do that as well, itās time you join Comsysto.