Jump to main content

Exploring Rust

Introduction

Rust is a multi-paradigm, high-level, general-purpose programming language designed for performance and safety, especially safe concurrency. It has been gaining popularity among developers worldwide and has been hailed as the “most loved language” in the Stack Overflow Developer Survey for several years running.

I’ve been toying around with Rust for a while now, and it’s about time I shared some of my insights. A quick disclaimer before we dive in: I’m still relatively new to the language and haven’t built a production app with it yet. But that doesn’t mean we can’t explore its fascinating features together!

In this article, we’ll delve into some of the key aspects of Rust that make it stand out from other languages, including zero-cost abstractions, memory safety through ownership and borrowing, algebraic data types (ADTs), polymorphism through traits, async/await, Cargo, and its vibrant community.

Zero-Cost Abstractions

One of the most appealing features of Rust is its zero-cost abstractions. This means that you can use high-level abstractions like generics, collections, iterators, and templates without any runtime overhead or performance penalties. However, this doesn’t apply to low-level abstractions like loops, conditions, or pointers.

Let’s take a look at an example:

let numbers = vec![13, 5, 8, 2, 1, 21, 3];
let max = numbers.iter().max();

match max {
    Some(&max) => println!("Found max value: {}", max),
    None => println!("Vector is empty"),
}

In this code snippet, we’re using an iterator abstraction to find the maximum value in a vector. We then use the Option enum type and pattern matching to handle the possibility of an empty vector. These abstractions work in concert to provide great performance and robust code without having to handle edge cases manually—hence the term “zero-cost”.

Zero-cost abstractions also play a significant role in error handling in Rust. While other languages provide mechanisms that force you to handle errors (like Go), Rust takes it a step further by making these mechanisms work in concert with other features.

Memory Safety

Rust provides memory safety guarantees without needing a garbage collector. This makes Rust a viable language for performance-critical applications.

Ownership

Ownership is one of Rust’s most fundamental features. It might be difficult to understand at first (especially if you’re coming from garbage-collected languages), but it solves many problems you’d otherwise have to deal with.

Ownership in Rust is based on the “Resource Acquisition Is Initialization” (RAII) design pattern (Wikipedia reference). In this pattern, resources are tied to object lifetimes—when an object is created, resources are allocated; when an object is destroyed, resources are released.

This helps avoid double-free runtime errors because only the single owner can release resources (implicitly). It also prevents resource leaks.

Here’s an example:

struct FileHandle {
	// system resource
	handle: Handle,
}

impl FileHandle {
	fn new(path: &str) -> Result<FileHandle, Error> {
		// resource acquisition
		let handle = File::open(path)?;
		// ownership is moved into FileHandle
		Ok(FileHandle { handle })
	}
}

fn do_something() -> Result<(), Error> {
	// ownership is acquired
	let file = FileHandle::new("data.csv")?;
	// ... do stuff with file
	Ok(())
}

// after `do_something`, `file` goes out of scope,
// and the system resource (Handle)
// will be released _automatically_

Borrowing

Borrowing in Rust allows you to have either one mutable reference or any number of immutable references at any given time. References must always be valid.

In our previous example with FileHandle, if we passed file around as an argument to other functions or methods without borrowing it first, we would move its ownership. This would invalidate previous variables that held file, which would violate Rust’s ownership rules.

The borrowing rules completely avoid data races and null-pointer dereferencing bugs.

Here’s an example:

fn do_something() -> Result<(), Error> {
	let mut file = FileHandle::new("data.csv");

	let file2 = &file;
	let file3 = &file;

	// this would be a compile-time error,
	// because we have immutable references (above)
	// _and_ mutable references (below)
	// let mut_file = &mut file;
	
	// ... do stuff with `file`
	Ok(())
}

Algebraic Data Types (ADTs)

Rust supports algebraic data types (ADTs), which are composite types created by combining variant types (enums/either of) and product types (structs/records). These work well with match expressions in Rust because match expressions are exhaustive—this prevents runtime errors if the variant type grows.

Here’s an example:

enum Shape {
    Rectangle { width: f64, height: f64 },
    Square { size: f64 },
    Circle { radius: f64 },
}

fn calculate_area(shape: &Shape) -> f64 {
    match shape {
        Shape::Rectangle { width, height } => width * height,
        Shape::Square { size } => size * size,
        Shape::Circle { radius } => PI * radius * radius,
    }
}

Polymorphism through Traits

Rust supports polymorphism through traits. Traits play well with Rust’s generic type system—they allow multiple types to implement the same trait without relying on inheritance hierarchies. This makes them non-invasive—you can add behavior without modifying the original implementation or inheritance hierarchy—and prevents the fragile base class problem where changes to base class behavior can affect derived classes.

The trait system uses static dispatch by default which enables efficient code generation.

Async/Await

Async/await in Rust works similarly to JavaScript—the async / await keywords are just syntactic sugar for working with Futures (Promises in JavaScript). However, there are some key differences:

  • Promises in JS are eager—they execute instantly even if they’re not awaited yet.
  • Futures in Rust are lazy—they only execute when needed.
  • Futures can be combined and composed with other Futures ahead of executing them without unnecessary overhead.
  • In Rust Futures can run in parallel using multiple threads since Rust isn’t single-threaded like JavaScript.

Here’s an example:

async fn get_post_data(post_id: u32) -> Result<PostData, Error> {
let api = ApiClient::new();

	let post = api.get_post(post_id).await?;
	let comments api.get_comments(post_id).await?;

	let user = api.get_user(post.user_id).await?;

	PostData {
		post,
		comments,
		user,
	}
}

Cargo

Cargo is Rust’s package manager—it does many things including building your project (cargo build), running your code (cargo run), testing your code (cargo test), managing your installed Rust version (rustup), handling your project’s configuration (Cargo.toml), and distributing packages (cargo publish).

Just like npm from JavaScript world your dependencies are listed in a Cargo.toml, alongside your package configuration. Public dependencies are pulled from crates.io, similar how npm fetches them from npmjs.com.

Community & Ecosystem

Rust has an active community that is growing rapidly. The community provides numerous resources such as official documentation (Rust Book), blog posts, YouTube videos and free courses for learning Rust. The community is open and friendly towards beginners as well as experienced developers.

The ecosystem around Rust is also growing rapidly and consists of high-quality libraries which makes working with Rust a joyous experience.

Promising Features

There are several aspects of Rust that I haven’t delved into yet but am excited about exploring further:

  • Macros / meta programming: This allows us to write code that generates/modifies other code during compilation.
  • Error system: The Result and Option types play very well with match expressions in Rust. I’m interested in experimenting more with these types especially in regards to Railway Oriented Programming but haven’t had the chance yet.

Conclusion

Rust offers several exciting features that set it apart from other programming languages—from zero-cost abstractions to memory safety through ownership and borrowing; from algebraic data types to polymorphism through traits; from async/await support to Cargo; all backed by a vibrant community and ecosystem.

While I found some aspects challenging initially—especially wrapping my head around concepts like ownership—I’ve come to appreciate how these features work together to help me write robust code more efficiently.

If you haven’t already done so I encourage you to explore what Rust has to offer—I’m sure you’ll find it as intriguing as I do! And remember—my journey with exploring Rust isn’t over yet; there’s still much more for me to learn!