marioph ✨

← Back

Rust Is Really Cool


  1. Immutability by default
  2. Pattern matching
  3. Error handling done right
  4. Null references: The billion dollar mistake
  5. Rust compiler teaches you
  6. Conclusion
  7. Other resources

As a web developer, mainly using TypeScript, PHP, etc, I wanted to explore a completely different language, stepping out of my comfort zone and exploring new ways of doing things.

Rust’s selling point for a large group of people is its performance, but I was particularly interested in its error handling and functional language features.

I am far from being an expert in Rust, but I would like to share the topics that have caught my interest the most, changing my perspective on software development.

Immutability by default

In Rust, variables are immutable by default. to declare a variable as mutable, you must use the mut keyword. Functions also need to declare if they intend to mutate their arguments.

We must be explicit about whether we are going to change the value of any variable, making our code more robust, predictable, and easier to reason about.

main.rs
let foo = 5;
let mut bar = 10;  // `bar` is mutable
 
show(&foo);
increment(&mut bar);  // Takes a mutable reference to `bar`
 
let fizz = vec![1, 2, 3, 4];
let mut buzz = vec![5, 6, 7, 8];
 
fizz.push(10)  // ✗
buzz.push(20)  // ✔

Pattern matching

Rust’s pattern matching syntax is a powerful tool for handling complex data types and writing robust and expressive code.

message.rs
enum Message {
	Quit,
	Move { x: i32, y: i32 },
	Write(String),
	ChangeColor(i32, i32, i32),
}

The match statement allow us to compare a value against a series of patterns and then execute code based on which pattern matches. We can represent all the possible states of our application and safely handle every scenario in a concise and expressive way.

message.rs
fn handle_message(msg: Message) {
	match msg {
		Message::Quit => println!("Goodbye!"),
		Message::Move { x, y } => println!("Move to {},{}", x, y),
		Message::Write(text) => println!("Text: {}", text),
		Message::ChangeColor(r, g, b) => println!("Color: {},{},{}"),
	}
}

Pattern matching is exhaustive, which means we must handle all possible cases, eliminating the risk of missing critical scenarios and potential bugs. Otherwise, the program would not compile.

Error handling done right

Rust treats errors as values. instead of throwing and error and hoping that some function above will catch it, if your code can produce an error, the compiler requires you to handle it or explicitly pass it to the next function.

Result is an enum which value can be either Ok or Err. each variant represents the success or failure of the operation, with its resulting value.

src/core/lib.rs
enum Result<T, E> {
    Ok(T),
    Err(E),
}

Consider this code that reads from a text file. The read_to_string function has a return type of Result. that indicates that the code might contain an error, thus forcing to handle it before using the result.

read_file.rs
use std::fs;
 
fn read_from_file(path: &str) {
    let read_result = fs::read_to_string(path);
 
    match read_result {
        Ok(content) => println!("Nice! {}", content),
        Err(error) => println!("Oops! {}", error),
    }
}

If we do not want to handle it now, we must modify the function signature to indicate that the result of this function could potentially be an error.

read_file.rs
use std::fs;
use std::io::Error;
 
fn read_from_file(path: &str) -> Result<String, Error> {
    fs::read_to_string(path);
}

By combining errors as values with enums and pattern matching, Rust ensures that we correctly handle all possible errors in our code.

read_file.rs
match error.kind() {
    ErrorKind::NotFound => ...,
    ErrorKind::PermissionDenied => ...,
    ErrorKind::FileTooLarge => ...,
}

Comparing it to TypeScript, for example, we can see the strength and correctness of the former solution.

In TypeScript, errors are thrown without any built-in mechanism to ensure that all error scenarios are properly handled or even if they will be handled at all.

error_handling.ts
try {
  // Do I need to handle a potential error?
  does_this_throw()
} catch (err: ???) { // Cannot infer the possible error types
	if (err instanceOf SomeError){...}
	if (err instanceOf OtherError){...}
}
 
// Who is responsible for handling the error?

There are a few libraries that try to solve this exact problem in TypeScript, but they are not part of the language itself, and they have performance implications, limitations and other caveats.

Null references: The billion dollar mistake

The problem with null values is that if you try to use a null value as a not-null value, you will get an error of some kind. because this null or not-null property is pervasive, it’s extremely easy to make this kind of error.

In his 2009 presentation “null references: the billion dollar mistake,” Tony Hoare, the inventor of null, has this to say:

I call it my billion-dollar mistake. at that time, I was designing the first comprehensive type system for references in an object-oriented language. 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. - Tony Hoare

The problem isn’t really with the concept of null values, but with the particular implementation. As such, Rust does not have nulls, but it does have an enum that can encode the concept of a value being present or absent.

Option is an enum that encodes the very common scenario in which a value could be something or it could be nothing. An Option type can be either Some, with a value, or None, that represents the absence of a value.

src/core/lib.rs
enum Option<T> {
    Some(T),
    None,
}

The Rust compiler ensures that you convert an Option<T> to a T before you can perform T operations with it. Generally, this helps catch one of the most common issues with null: assuming that something isn’t null when it actually is.

find_haystack.rs
fn find(haystack: &str, needle: char) -> Option<usize> {
    for (index, c) in haystack.char_indices() {
        if c == needle {
            return Some(index);  // We've found the index of the needle!
        }
    }
    None  // We didn't found it, there is no index to return
}
 
fn main() {
    let result = find("hello", 'l');
    match result {
        Some(index) => println!("Found at index {}", index),
        None => println!("Not found"),
    }
}

Rust compiler teaches you

Learning Rust can be quite challenging. understanding concepts such as the ownership model, string types, generics, traits, lifetimes, macros, and complying with the strict and authoritative rules of the language, can be overwhelming. However, one thing that made my learning journey easier was to learn from the compiler errors.

Initially, you may make several fundamental mistakes because you do not yet understand how Rust works. The compiler is very strict, but it is also extremely useful, showing precisely where the error occurred, the probable solution to it, and a brief explanation along with the corresponding error code.

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src/main.rs:18:5
   |
16 |     let word = first_word(&s);
   |                           -- immutable borrow occurs here
17 |
18 |     s.clear(); // error!
   |     ^^^^^^^^^ mutable borrow occurs here
19 |
20 |     println!("the first word is: {}", word);
   |                                       ---- immutable borrow later used here
 
For more information about this error, try `Rustc --explain E0502`.
error: could not compile `ownership` due to previous error

Conclusion

Rust is unfamiliar, and can be tough to learn, but it’s worth the effort. it is a strict language with a steep learning curve; however, this design choice helps developers understand its complexity early on and avoid bugs in the future. Keep this in mind before starting to learn Rust.

Rust is a robust, elegant, and powerful language. Although I will probably not use it at work, I have found it a very enjoyable language to learn, despite being quite challenging. It has changed my view on software development and how things can be done, and most importantly, it has been really fun.🦀

Other resources

Most of the code examples are taken from the Rust book, and other cool resources.