Rust - And The Joy Of Learning New Languages
Learning a new programming language is like stepping into an entirely new world—a world where I know nothing, where everything is unfamiliar, and every discovery brings a sense of accomplishment. It’s a thrilling place to be, full of opportunities to explore new paradigms and revisit old concepts from fresh perspectives. Recently, I found myself drawn into the world of Rust, a language that has been gaining attention for its emphasis on memory safety and performance.
My fascination with Rust feels like coming full circle in many ways. At the very start of my career, I worked with C and C++—building visuals with OpenFrameworks and experimenting with OSC, the Open Sound Control protocol. These projects let me combine my love for coding and music, exploring the intersection of art and technology. Later, I delved into programming microcontrollers like AVRs, PICs, and Arduinos, dabbling in C and even some Assembly for small-scale embedded projects. That hands-on experience with low-level programming gave me a deep appreciation for the intricacies of how software interacts with hardware.
Now, years later, Rust has reignited that spark. My journey began with The Rust Programming Language - Second Edition, a book consistently recommended to me by the Amazon recommendation engine 😂. What strikes me immediately when picking up a new language is how it transports me back to the "beginner's mindset"—a place where I can embrace the challenge of starting from scratch, peeling back the layers of a language I have never touched before. The book dives deep into low-level concepts, reminiscent of my earlier experiences with C/C++, but with a modern twist: safety and clarity at the forefront.
Rust is unique not only because of its features but because of its philosophy. As I’ve worked through the chapters, I’ve come to appreciate why this language exists, the problems it aims to solve, and the way it combines familiar constructs with innovative ideas. In this article, I want to share what I’ve discovered so far—both the familiar and the novel—about Rust, and why this journey has been so rewarding.
A Brief History of Rust and Why It Exists
Rust's story begins with Mozilla, the organization best known for the Firefox web browser. In 2010, Mozilla employee Graydon Hoare created Rust as a personal project, aiming to address some of the challenges faced by languages like C and C++. These languages, though incredibly powerful and versatile, have long been plagued by issues like memory safety errors and undefined behavior. Hoare’s vision was to create a systems programming language that maintained the low-level control and performance of C/C++, but without compromising on safety and reliability.
The language gained traction when Mozilla adopted it for internal use, particularly in the development of Servo, a new web rendering engine. Servo became a proving ground for Rust, demonstrating how it could handle complex, performance-critical tasks while avoiding common programming pitfalls like null pointer dereferences and data races. By 2015, Rust had matured enough for its first stable release, and it quickly gained a reputation as a modern systems programming language built for safety, concurrency, and performance.
What makes Rust stand out is its philosophy. At its core, Rust is designed to help developers write code that is not only efficient but also correct. It achieves this through features like its ownership model, which enforces strict rules about how memory is managed, and its strong type system, which eliminates whole classes of bugs at compile time. Rust's ecosystem is bolstered by tools like Cargo, its package manager and build system, which makes dependency management and project setup a breeze.
But why does Rust exist in the first place? To understand this, it helps to look at the problems it aims to solve:
- Memory Safety: Rust prevents issues like buffer overflows and use-after-free errors without relying on garbage collection.
- Concurrency: Rust's ownership system allows developers to write concurrent programs without worrying about data races.
- Performance: Rust delivers C/C++-like performance, making it suitable for resource-intensive tasks like game engines, web browsers, and operating systems.
Rust isn't just a tool for experts in low-level programming—it’s a language that encourages good practices and helps developers learn how to write safer, more efficient code. Its growth in popularity is a testament to its ability to bridge the gap between high performance and high reliability.
Learning Rust Through the Book
When I picked up the book, I wasn’t just looking for a tutorial—I was looking for a guide into the mindset behind Rust. The book didn’t disappoint. It feels like it’s written for learners who want to dig deep, offering not just "how-tos" but also the "whys" of the language. The experience reminded me of the time I first worked with C/C++, where every new concept felt like uncovering a fundamental truth about how computers work.
The book starts by introducing Rust in a way that emphasizes its systems-level nature. Early chapters dive into concepts like ownership, borrowing, and lifetimes—features that make Rust distinct. It’s not just about writing code that works; it’s about writing code that’s safe and predictable at a level that most modern languages abstract away. This isn’t a lightweight "get up and running in an hour" tutorial. Instead, it’s a structured journey that ensures you understand the mechanics of what’s happening under the hood.
What stood out to me early on was how much the book focuses on why Rust has its unique approach to memory and safety. Concepts like the ownership model, which I’ll dive into later, are introduced with examples and analogies that make it easier to grasp. At times, it feels like a C or C++ programming guide, particularly when it discusses low-level topics like stack and heap memory, but the tone is approachable and modern, with a focus on avoiding pitfalls rather than just navigating them.
The book also intersperses practical exercises and projects to reinforce each concept, which is something I found incredibly helpful. For example, creating small programs that manipulate strings, handle collections, or implement simple algorithms made each chapter feel like a step forward, not just a theoretical exercise.
What I appreciate most is that it doesn’t shy away from Rust’s complexities. Instead, it guides you through them at a deliberate pace, ensuring you understand not just what Rust is doing, but also why it does it that way. As someone who has worked with a variety of languages over the years, I found myself constantly nodding along, thinking, "Ah, that’s clever," or "That makes so much sense."
I’m currently on Chapter 9, where the focus shifts to error handling—a crucial aspect of Rust’s design philosophy. But even at this point, it’s clear that the book’s goal isn’t just to teach Rust syntax. It’s to help you think like a Rust programmer, embracing its focus on safety, performance, and clarity.
Rust’s Approach to Safety and Preventing Bugs
One of the first things you notice when learning Rust is its relentless focus on safety. Unlike many other languages, Rust doesn’t allow you to ignore potential issues—it forces you to address them upfront. While this might seem frustrating at first, it quickly becomes one of its most empowering features. The language is designed to help you write code that’s not just functional, but fundamentally reliable.
At the heart of Rust’s safety philosophy is its ownership model. This system governs how memory is allocated, accessed, and released, ensuring that common issues like null pointer dereferences, use-after-free errors, and data races are impossible to write (at least, without unsafe blocks). Rust achieves this without relying on garbage collection, offering both performance and safety—a combination that’s rare in the programming world.
Another standout feature is Rust’s approach to error handling. Unlike many languages where error states can be ignored or forgotten, Rust makes error management explicit. Functions that can fail return a Result
type, which requires you to handle both success and failure cases. This explicitness eliminates an entire class of bugs caused by unhandled exceptions or ignored error states. For example:
fn main() {
let file = File::open("hello.txt");
match file {
Ok(file) => println!("File opened successfully!"),
Err(error) => println!("Failed to open file: {:?}", error),
}
}
Rust also introduces features like lifetimes to ensure references are always valid. At first glance, this might seem overly complex, but it’s a necessary trade-off to ensure that memory safety is guaranteed without sacrificing performance. It’s a bit like having a strict but fair teacher—they might seem demanding at first, but you come to appreciate their rigor when you see the results.
Another aspect I love is how Rust prevents data races in concurrent programs. Data races—situations where two threads access the same memory location simultaneously, with at least one write—are notoriously difficult to debug. Rust’s ownership model and type system make it impossible to write code with data races, forcing you to handle concurrency in a safe, structured way.
These safety measures aren’t just about catching bugs—they’re about encouraging a mindset of careful, deliberate programming. They push you to think critically about your code, fostering habits that will carry over into any language you work with. The compiler might feel like a strict guardian at times, but it’s always working in your favor.
Rust’s safety features go beyond theoretical benefits. They’re the reason the language has been embraced by industries where reliability is non-negotiable, from embedded systems to operating systems and beyond. It’s a language that doesn’t just help you write code—it helps you write good code.
Familiar Features in Rust
One of the joys of learning Rust has been spotting familiar concepts—features I’ve encountered in other languages, now appearing with a Rust twist. It’s fascinating to see how ideas travel across languages, evolve, and reappear in new contexts. Rust feels like a crossroads, where many of these ideas converge in a way that feels both comfortable and intriguing.
Take pattern matching, for example. Rust’s match
expression provides a way to evaluate a value against patterns and handle different cases concisely. Here’s a simple example:
let number = 3;
match number {
1 => println!("One!"),
2..=5 => println!("Between two and five!"),
_ => println!("Something else!"),
}
This reminded me immediately of PHP's match
expression, which simplifies conditional logic in a similar way:
$number = 3;
$result = match ($number) {
1 => "One!",
2, 3, 4, 5 => "Between two and five!",
default => "Something else!",
};
echo $result;
Ruby’s case
statement also came to mind. Although not identical, it achieves a similar result using ranges and patterns:
number = 3
result = case number
when 1 then "One!"
when 2..5 then "Between two and five!"
else "Something else!"
end
puts result
Another feature that stood out was Rust’s module system, which felt very familiar from working with Ruby and Python. Modules allow you to group related functionality and control what’s exposed to the outside world. In Rust, you might organize your code like this:
mod utilities {
pub fn greet(name: &str) {
println!("Hello, {}!", name);
}
}
fn main() {
utilities::greet("Alice");
}
This brought back memories of how I’d write similar code in Python, using a separate file for utilities:
# utilities.py
def greet(name):
print(f"Hello, {name}!")
# main.py
import utilities
utilities.greet("Alice")
Rust also supports implicit returns, where the last expression in a function is returned without the return
keyword. This felt intuitive, as it mirrors the behavior in Ruby:
fn add(a: i32, b: i32) -> i32 {
a + b // Implicit return
}
def add(a, b)
a + b # Implicit return
end
Other familiar features include slices, which are a way to work with subsets of collections. In Rust, slices look like this:
let array = [1, 2, 3, 4, 5];
let slice = &array[1..3]; // [2, 3]
Seeing this reminded me of similar functionality in JavaScript:
const array = [1, 2, 3, 4, 5];
const slice = array.slice(1, 3); // [2, 3]
What fascinates me is not which language does it better, but the shared vocabulary across programming. Features like pattern matching, modules, implicit returns, and slices are like recurring motifs in a piece of music—each language offering its interpretation. Rust’s familiarity, combined with its unique touches, makes learning it a delightful journey of recognition and discovery.
Discovering Rust’s Unique Features
While Rust offers many familiar constructs, it also introduces concepts that feel entirely new, at least to me. These unique features often push you to think differently about programming, particularly when it comes to safety, memory management, and clarity. They’re some of the reasons Rust stands out as a language and why learning it has been so rewarding.
One of the first things that caught my attention was Rust’s approach to mutability. In Rust, variables are immutable by default, meaning once a value is assigned, it cannot be changed. To allow mutation, you must explicitly declare the variable as mutable using the mut
keyword:
let mut x = 5;
x = 10; // Allowed because x is mutable
This feels like a small change, but it has a big impact. It encourages you to think carefully about when and why a variable should change, leading to code that’s easier to reason about.
Another fascinating concept is variable shadowing. Shadowing allows you to reuse a variable name within the same scope, effectively overwriting the previous value. However, rather than mutating the original variable, shadowing creates a new one, letting you safely transform data without unintended side effects:
let x = 5;
let x = x + 1; // Shadowing the previous x
let x = x * 2;
println!("The value of x is: {}", x); // Output: 12
Rust’s ownership model is perhaps its most talked-about feature—and for good reason. Ownership governs how memory is allocated, accessed, and freed in Rust. Each value in Rust has a single owner, and when the owner goes out of scope, the value is automatically cleaned up. Ownership, combined with borrowing and lifetimes, ensures memory safety without the need for a garbage collector. For example:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // Ownership is moved to s2
// println!("{}", s1); // This would cause a compile-time error
}
At first, the ownership model feels strict, but as you work with it, you begin to appreciate how it eliminates whole classes of bugs like use-after-free errors.
Rust also introduced me to vectors, which are like dynamic arrays. Unlike arrays, vectors allow their size to grow or shrink as needed, and their values are stored contiguously in memory, making them efficient. Here’s an example of a vector in action:
let mut vec = Vec::new();
vec.push(1);
vec.push(2);
vec.push(3);
for num in &vec {
println!("{}", num);
}
Another unique feature is Rust’s emphasis on safety through type inference and detailed compile-time checks. For instance, lifetimes might initially feel complex, but they ensure references are always valid, eliminating dangling pointers or memory corruption. Although I’m still getting used to lifetimes, I can already see how they contribute to Rust’s goal of safe and predictable programming.
These features, while new to me, have quickly become some of my favorite aspects of Rust. They encourage a mindset where safety, clarity, and intent take precedence, shaping not just how you write Rust, but how you think about programming in general.
Error Handling
As of now, I’ve made my way to Chapter 9, where the focus shifts to error handling. It’s fascinating to see how Rust tackles this fundamental aspect of programming, emphasizing clarity and safety even in the face of unexpected outcomes. Functions in Rust return a Result
type for operations that can fail, making error handling an explicit part of the code. This approach ensures that you can’t just “forget” to deal with potential issues—a habit that I’ve seen lead to trouble in other languages.
For example, imagine a scenario where a restaurant booking system needs to handle various cases: tables being fully booked, invalid table numbers, or incorrect booking times. Rust’s Result
type makes it straightforward to deal with these possibilities explicitly:
use std::collections::HashMap;
#[derive(Debug)]
enum BookingError {
FullyBooked,
InvalidTable,
InvalidTime,
}
fn book_table(table: u8, time: u8) -> Result<String, BookingError> {
let mut tables: HashMap<u8, bool> = HashMap::new();
tables.insert(1, false); // Available
tables.insert(2, true); // Fully booked
tables.insert(3, false); // Available
if !tables.contains_key(&table) {
return Err(BookingError::InvalidTable);
}
if time < 18 || time > 22 {
return Err(BookingError::InvalidTime);
}
if tables[&table] {
return Err(BookingError::FullyBooked);
}
Ok(format!("Table {} booked at {}:00!", table, time))
}
fn main() {
let booking1 = book_table(1, 19); // Valid booking
let booking2 = book_table(2, 20); // Fully booked
let booking3 = book_table(4, 21); // Invalid table
let booking4 = book_table(1, 17); // Invalid time
for booking in [booking1, booking2, booking3, booking4].iter() {
match booking {
Ok(success) => println!("{}", success),
Err(BookingError::FullyBooked) => println!("Booking failed: Fully booked."),
Err(BookingError::InvalidTable) => println!("Booking failed: Invalid table."),
Err(BookingError::InvalidTime) => println!("Booking failed: Invalid time."),
}
}
}
The output of this program would look like this:
Table 1 booked at 19:00!
Booking failed: Fully booked.
Booking failed: Invalid table.
Booking failed: Invalid time.
In this example, the book_table
function returns a Result<String, BookingError>
, where the success case provides a confirmation message and the failure cases specify why the booking couldn’t be completed. Using the match
construct, each error is handled explicitly, making it clear what went wrong and why. This explicitness fosters a sense of responsibility for handling every possible outcome—a principle Rust enforces consistently.
Looking back at what I’ve learned so far, it’s clear that Rust doesn’t shy away from its complexities. Concepts like ownership, borrowing, and lifetimes required me to pause, reread, and experiment more than I anticipated—but the payoff has been worth it. The strictness of the compiler, which initially felt like a barrier, is now something I appreciate deeply. It’s like having a skilled mentor constantly watching over your shoulder, catching mistakes before they can cause problems.
At this point, I’m starting to see the bigger picture of why Rust exists and how its features work together. The language’s focus on safety and performance isn’t just about preventing bugs; it’s about encouraging a disciplined and thoughtful approach to programming. I’m excited to dive deeper into error handling and uncover more of Rust’s philosophy as I progress through the book.
Closing Thoughts
Learning Rust has been a journey unlike any other language I’ve explored. It’s not just about the syntax or the tools—it’s about embracing a philosophy that prioritizes safety, clarity, and performance. Rust challenges you to be deliberate in your choices, to think deeply about ownership, and to handle errors as a natural part of coding. It’s a mindset that has influenced not just how I write Rust code, but how I approach programming as a whole.
What stands out most about Rust is how it brings together ideas from so many places. Whether it’s pattern matching, slices, or modules, there’s a sense of familiarity that bridges the gap between Rust and other languages I’ve worked with. At the same time, Rust’s unique features—its ownership model, explicit mutability, and compile-time checks—feel like a glimpse into the future of programming. It’s rare to find a language that balances familiarity and innovation so seamlessly.
For me, learning Rust has been about rediscovering the joy of not knowing. Starting from scratch, fumbling through errors, and celebrating small victories has been an exhilarating experience. There’s a unique satisfaction in wrestling with a problem, understanding why the compiler refuses to budge, and finally arriving at a solution that feels solid and well-considered.
Rust matters because it’s reshaping how we think about software. It’s proving that we don’t have to compromise between performance and safety, or between expressiveness and control. It’s a language designed for the long term, one that helps developers write code that’s not only functional but also correct and maintainable.
As I continue this journey, I find myself not just learning Rust, but growing as a programmer. I believe that Rust will challenge me to write better code, to plan more carefully, and to think critically about the trade-offs I make. It’s not just a tool—it’s a teacher. And that, to me, is what makes it worth learning.
If you’ve ever considered diving into Rust, I encourage you to give it a try. Whether you’re drawn by its safety features, its performance, or simply the thrill of learning something new, you’ll find that Rust has a way of leaving a lasting impression—not just on your code, but on how you think about programming itself.