Half a Year of Rust šŸ¦€

Korbinian Schleifer
comsystoreply
Published in
15 min readDec 20, 2023
The Rust logo: a crab called crustacean on a blue background

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

  1. šŸ” Static Typing
  2. šŸ“Ž Concise & Expressive Syntax
  3. šŸš€ Performance
  4. šŸ’¾ Concept of Ownership & Memory Safety
  5. šŸ› 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.

The graph shows the difficulty of learning Rust compared to JavaScript. Initially learning JavaScript is easier than Rust. But in the long term Rust becomes easier compared to JavaScript as it gives you a lot of benefits.
The path of learning Rust vs JavaScript

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:

  1. 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.

Bonus Material

--

--