All Articles

Why Developers Love Rust

Rust has been getting a lot of media attention recently. It seems like a "X written in Rust" post makes the front page of hackernews every other day. Rust has been voted the most loved language for five years running, and it grew in use on Github by 235% from 2018 to 2019. Large companies such as Mozilla, Apple, Amazon, Facebook, Google, Twitter, and Microsoft have began adopting it in their codebases. So, why do so many people love Rust?

Rust was built to solve many of the hassles associated with other popular languages. Let's look at a couple of examples:

Memory Safety

Rust focuses on speed and safety. It balances speed and safety through many ‘zero-cost abstractions’. This means that in Rust, abstractions cost as little as possible in order to make them work. The ownership system is a prime example of a zero cost abstraction. All of the analysis we’ll talk about in this section is done at compile time. You do not pay any run-time cost for any of these features.

To track the ownership of each value: a value can only be used at most once, after which the compiler refuses to use it again.

For example, the following code:

fn main() {
    let original = String::from("hello");
    takes_ownership(original);
    println!("{}", original)
} 

fn takes_ownership(other: String) {
    println!("{}", other);
} 

Yields an error:

error[E0382]: borrow of moved value: `original`
 --> src/main.rs:4:20
3 |   takes_ownership(original);
  |                     - value moved here
4 |   println!("{}", original)
  |                    ^ value borrowed here after mov

In the above code, the ownership of original was moved to the take_ownership function. Because the ownership was moved, Rust now cleans up the memory of original. Now, the compiler prevents you from using original.

Rust's ownership model guarantees, at compile time, that your application will be safe from dereferencing null or dangling pointers This prevents the dreaded double-free regularly encountered in C or C++, along with many other memory related issues.

In Rust, functions can borrow ownership of a value. Rust tracks borrowed ownership with the borrow checker. We can modify the example above to borrow original, instead of taking ownership:

fn main() {
    let original = String::from("hello");
    borrow_ownership(&original);
    println!("{}", original)
} 

fn borrow_ownership(other: &String) {
    println!("{}", other);
}

Now the code compiles, because the ownership of original stays in the main function. Instead of owning the resource, the function borrows ownership. We call the &T type a ‘reference’. A binding that borrows something does not deallocate the resource when it goes out of scope. This means that after the borrow, we can use our original bindings again.

Rust memory safety comes at the cost of complexity. New developers often complain that getting a program to compile can be quite difficult. It’s pretty common for newcomers to the Rust community to get stuck "fighting the borrow checker". As Rust learner explained:

"It's hard but I love it. Dealing with the compiler felt like being the novice in an old kung fu movie who spends day after day being tortured by his new master (rustc) for no apparent reason until one day it clicks and he realizes that he knows kung fu."

Fighting the borrow checker can be frustrating, but trust me, it's worth it. Rust is often compared to Haskell and Scala in the sense that if your code compiles, you can sleep at night without having to worry about runtime errors. This is even more true after looking at the memory safety Rust enforces through its ownership model.

Rust also has a second language hidden inside it that doesn’t enforce memory safety guarantees: it’s called unsafe Rust. Wrapping code with the unsafe block effectively tells the compiler to shut up, because you know what you are doing. Doing so gives you unsafe superpowers. For example, you can dereference a raw pointer:

let mut num = 5;

let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

unsafe {
  println!("r1 is: {}", *r1);
  println!("r2 is: {}", *r2);
}

If you can't do something in safe Rust, you can implement it yourself with unsafe. However, unsafe should be used with caution. Abusing it can have unwanted consequences. Because of this, Rust forces you to explicitly mark code as unsafe. You cannot use an unsafe function in a safe block. Many developers even opt to mark there entire project with ![forbid(unsafe_code)].

Rust vs. Dynamic Languages

Developers coming from dynamically typed languages will find it hard to argue the benefits of static typing. Static type definitions are even being added to many popular dynamic languages, such as javascript's typescript, python's type hints, and ruby's rbs. Static languages are generally considered more "scalable" and better for larger codebases as the compiler does much of the work for you. Let's look at an example:

def silly(a)
  if a > 0
    puts 'hello'
  else
    print a + '3'
  end
end

The code above prints 'hello', right? Let's test it out:

$ silly(2)
=> "hello"

But, when you pass a negative number:

$ silly(-1)
=> TypeError (String can't be coerced into Integer)

You get a TypeError at runtime.

A simple mistake like this can cause runtime errors that can be hard to debug without comprehensive test coverage. Since Rust is statically typed, all type errors will be caught at compile time, and this problem never occurs.

Static typing also results in compiled code that executes faster as the compiler knows the exact data types that are in use, and therefore can produce optimized machine code.

The points in this section apply to pretty much all strongly typed languages. Now let's look at some of the things Rust does differently than other statically typed languages.

No Nulls

Most languages have a concept of null. Any value can either be what you expect, or nothing at all. If you accidentally miss a null check, you code can blow up at runtime. Tony Hoare, the inventor of null references had this to say about the concept:

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years

In Rust, dereferencing of raw pointers is an unsafe operation. So if you try using a value that has been assigned to null:

let p: *const i32 = std::ptr::null();
println!("{}", *p);

Your code will not compile:

error[E0133]: dereference of raw pointer is unsafe and requires unsafe function or block
 --> src/main.rs
  |
5 |     println!("{}", *p);
  |                    ^^ dereference of raw pointer
  |
  = note: raw pointers may be NULL, dangling or unaligned; they can violate aliasing rules and cause data races: all of these are undefined behavior

Again, Rust is taking steps to prevent undefined behavior. This is a sigh of relief, as you will no longer be encountering a NullPointerException, or foo is undefined errors as you might be used to from other languages.

Instead of using null, Rust expresses optional values with an type called Option:

pub enum Option<T> {
    None,
    Some(T),
}

An Option is either something, or nothing. This union is expressed succinctly with Rust enum's, which can hold values. You can pattern match on an option enum to access the underlying value:

match x {
  None => handle_none(),
  Some(value) => return value
}

But what happens if you forget to check for None? Doesn't this pose the same problems as null? Nope! Rust solves this problem my enforcing exhaustive pattern matching. This means that this code, which does not check for None:

match x {
  Some(value) => println!("{}", value)
}

Will not compile:

error[E0004]: non-exhaustive patterns: `None` not covered
--> src/main.rs:6:11
  |
6 |  match x {
  |  ^^^^^^ pattern `None` not covered
  |
  = help: ensure that all possible cases are being handled, 
    possibly by adding wildcards or more match arms

In Rust, the code above would never make it to production, and clients would never experience the error because the compiler is so strict. Also note how detailed the error message is, telling you the exact location, problem, and potential solution to the error.

Rust vs. Statically Typed Languages

Rust does its best to get out of the developer's way when it comes to static typing. Rust has a very smart type inference engine. It looks not only at the type of the value expression during its initialization but also at how the variable is used afterwards to infer its type. However, Rust's use of type inference does not decrease its ability to provide detailed error messages at compile time. Let's see how that type inference works.

We can start by initializing a integer:

let elem: u8 = 5;

Because of the annotation, the compiler knows that elem is of type u8. Now we can create a mutable vector (a growable array):

let mut vec = Vec::new();

At this point the compiler doesn't know the exact type of the vector. It just knows that it's a vector of something (Vec<_>). But once we insert the element into the vector

vec.push(elem);

Aha! Now the compiler knows that vec is a vector of u8's (Vec<u8>)

No type annotation of variables was needed, the compiler is happy and so is the programmer!

Rust vs. Garbage Collected Languages

Garbage collection is an automatic memory management system that looks for unused variables and frees their memory. It is a concept employed by many widely used languages, such as Java, Ruby, and Python. However, garbage collection can introduce performance issues at scale.

For example, Discord used Golang, a garbage collected language, for keeping track of which channels and messages a user read. They began experiencing latency and CPU spikes consistently every 2 minutes. This is because Go will force a garbage collection run every 2 minutes, scanning the entire LRU cache to determine which memory needed to be handled by GC.

Here is a before and after of them switching from Go, to Rust. Go is purple, Rust is blue.

Read the full post here: Why Discord is Switching from Go to Rust

Why is Rust so much better? Rust is blazingly fast and memory-efficient without needing a garbage collector, due to its ownership model. Here is a simple example:

// s is not valid here, it’s not yet declared
{
  let s = "hello"; // s is valid from this point forward
  // do stuff with s
}
// this scope is now over, s is no longer valid 
// and will be freed from memory

Thanks to Rust's ownership tracking, the lifetime of ALL memory allocated by a program is strictly tied to one function, which will ultimately go out of scope. This also allows Rust to determine when memory is no longer needed and clean it up at compile time, resulting in efficient usage of memory and more performant memory access.

Skylight, an early adopter of Rust was able to reduce their memory usage from 5GB to 50MB by rewriting certain endpoints from Java to Rust.

Rust vs. Other Systems Programming Languages

Rust was built by Mozilla to be a the next step in the evolution of C or C++, two other systems programming languages. Rust gives you the low level control, while still providing features and conveniences that make it feel like a high-level languages. It gives you the technical power without allowing it to degrade from the developer experience.

Unlike something like Ruby, which disregards performance for developer experience, or C, which takes a more barebones approach, Rust provides as many zero-cost abstractions as possible; abstractions that are as performant as the equivalent hand-written code.

For example, here is how you would create an array containing the first ten square numbers in C:

int squares[10];
for (int i = 0; i < 10; ++i)
{
  squares[i] = i * i;
}

And the equivalent code in Rust, using an iterator:

let squares: Vec<_> = (0..10).map(|i| i * i).collect();

As you can see, Rust provides high level concepts with ergonomic interfaces. It does this while still being highly performant.

The Rust Ecosystem

Rust has become larger than just a language, it has a large ecosystem and community supporting it.

Rust provides rustup, an official language installer. It allows you can manage multiple installations and easily switch between stable, beta, and nightly compilers. It also makes cross compiling between multiple platforms simpler.

Rust also provides cargo, a tool for managing a Rust packages dependencies, running tests, generating documentation, compiling your package. Rust packages or "crates" created with cargo can be published to crates.io and made available for use by anyone. There are currently almost 50,000 available crates, and over 3.5 Billion downloads! Any library published to crates.io will have its documentation automatically built and published to docs.rs.

Unlike many languages, there is an official tool for formatting Rust code in rustfmt, as well as clippy, the linter that helps catch common mistakes and improve your code.

Rust has an extremely friendly and welcoming community. This is a breath of fresh air coming from other languages, such as [insert unfriendly community here]. You can reach out through the discord chat, forum, subreddit, stackoverflow tag, slack channel, or gitter.

There are a ton of opensource projects created by the community. From web frameworks such as actix web, yew, and rocket, to Rust based text editors like remacs and xi editor. Even the language itself is opensource, and has 50,000 stars and over 3,000 contributors.

For a full list of resources, see Awesome Rust, a curated list of Rust code and resources.

Rust and WebAssembly

Another reason that people get so excited about Rust is how well it plays with WebAssembly. Webassembly is a binary instruction format that can run in most major browsers. It aims to execute at native speed by taking advantage of common hardware capabilities available on a wide range of platforms. Wasm can be run in the place of, or alongside traditional javascript, allowing developers to offload performance critical tasks from javascript, improving their application's performance without having to completely rewrite their existing codebase.

Rust can be compiled directly into WebAssembly and run in the browser with Cargo:

$ cargo build --target=wasm32-unknown-emscripten

This allows you to take advantage of all Rust's compile safety in the web. Since Rust lacks a runtime, generated .wasm files are very small because there is no extra bloat included like a garbage collector. Rust and WebAssembly integrates with existing javascript tooling. It supports ECMAScript modules and other tools such as npm packages and webpack.

There are some really cool Rust + Wasm projects out there. For example, Yew lets you create multi-threaded front-end web apps with Rust, in a way that feels almost like React.js.

For more information regarding Rust and WebAssembly, see the rustwasm book

Getting Started

Hopefully you understand why Rust is such a beloved language by developers. To get started with learning Rust, you should check out the Rust book. For other learning options and hands-on projects, click here