Rust: Not Just Zoom Zoom Fast
Tags: rust, functional, programming
Published: 2023-02-25 14:28:06

When it comes to Rust, the first thing that usually comes to mind is its impressive performance. And while Rust certainly delivers on this front, there's so much more to the language than just raw speed. From its well-designed syntax and powerful abstractions, to its robust package manager and vibrant ecosystem, Rust is a language that truly has it all. In this post, we'll take a closer look at some of the key features that make Rust such a versatile and compelling language.

<br />

While the language features discussed in this post may not be exclusive to Rust, it is the way in which they are carefully designed and integrated that sets Rust apart. Rust is the only language where these features converge seamlessly to create a coherent system, which is why it is such a captivating language.

<br />

## Practical Immutability

<br />

In Rust, variables can be declared as either immutable or mutable using the `mut` keyword. Rust embraces immutability as a default by making variables immutable by default. This means that we must explicitly declare variables as mutable if we need to change their value later. This approach makes it easier to reason about the behavior of programs and helps prevent accidental mutations.

```rust
let x = 5; // immutable
let mut y = 5; // mutable

y = 6; // ok
x = 6; // error: cannot assign twice to immutable variable `x`
```

In addition to mutable variables, Rust supports passing mutable references to functions. Functions must declare whether they intend to mutate their arguments or not, which further helps prevent accidental mutations. This allows us to use mutable variables in a controlled, explicit way.

```rust
fn increment(num: &mut i32) {
    *num += 1; // dereference the pointer to mutate the value
}

fn main() {
    let mut x = 10; // x is mutable
    increment(&mut x); // pass a mutable reference to x

    println!("x: {}", x); // prints "x: 11"
}
```

By embracing immutability as a default and using mutable variables only when necessary, Rust code becomes more robust and predicatable.

<br />

## Algrebraic Data Types (ADTs) and Pattern Matching

<br />

Algebraic Data Types (ADTs) are a fundamental concept in functional programming that allow for the creation of complex data types by combining simpler types. ADTs can be of two types: Sum types and Product types. Sum types combine multiple types into a single type that can hold one of the constituent types at any given time. Rust provides a powerful implementation of Sum types in the form of enums or enumerated types. Structs, on the other hand, are used to represent Product types. You've probably already used Product types in other languages (interfaces in TypeScript, classes in Java, etc.).

<br />

For example, consider a program that represents the types of shapes. We can use an enum to represent the different types of shapes:

```rust
enum Shape {
    Circle(f64),
    Rectangle(f64, f64),
    Triangle(f64, f64, f64),
}
```

Here, we have defined an enum `Shape` that has three variants: `Circle` that takes a single `f64` argument representing the radius, `Rectangle` that takes two `f64` arguments representing the length and width, and `Triangle` that takes three f64 arguments representing the lengths of its three sides. This allows us to represent any possible shape in a single data type.

<br />

Now we can use pattern matching to easily and safely parse the data of a shape. If you don't know what pattern matching is, think of it as a `switch` statement on steroids. It allows us to match a value against a pattern and execute code based on the pattern that matches. I have a [whole blog post on pattern matching btw](https://dev.to/nexxeln/expressive-code-with-pattern-matching-3de6).

```rust
fn area(shape: Shape) -> f64 {
    match shape {
        // pi * radius^2
        Shape::Circle(radius) => std::f64::consts::PI * radius * radius,

        // length * width
        Shape::Rectangle(length, width) => length * width,

        // Heron's formula: sqrt(s * (s - a) * (s - b) * (s - c)) where s = (a + b + c) / 2
        Shape::Triangle(side1, side2, side3) => {
            let s = (side1 + side2 + side3) / 2.0;
            (s * (s - side1) * (s - side2) * (s - side3)).sqrt()
        }
    }
}
```

Rust's compiler also ensures that our pattern matching is exhaustive, meaning that we must handle all possible cases. This prevents us from accidentally forgetting to handle a case and causing a runtime error.

![Exhaustive pattern matching](<https://us-east-1.tixte.net/uploads/nexxel-has.no-friends.xyz/Alacritty-v0.10.0-portable_(1)_VPpISCD8Jv.png>)

ADTs along with pattern matching make it trivial to create and handle complex data types in a safe and concise way.

<br />

## Built-In Abstractions

<br />

Rust provides powerful built-in abstractions that help in writing correct and safe code easily. Two of the most important abstractions in Rust are `Option` and `Result`. These types are essential for working with Rust's null-safety and error-handling systems, which help prevent bugs and improve the reliability of Rust programs. In this section, we'll explore `Option` and `Result` in detail, and see how they can be used to write more reliable and error-free Rust code.

<br />

### No Null, No Problem (Option)

<br />

Rust replaces the concept of null with the `Option` type, providing a safer alternative that eliminates the risks associated with null values. `Option` is an enum that can be either `Some` with a value or `None` to represent absence of a value. This type-safe approach allows us to handle absence of a value without resorting to null. Here's how the `Option` type is defined in Rust:

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

By using `Option`, we can ensure that our code is free from null-related bugs and errors, making it easier to reason about program behavior. You may be familiar with this pattern from other languages like Haskell's `Maybe` monad or OCaml's `option` type.

<br />

Let's look at `Option` in practice. Consider a function that takes a vector of integers and returns the largest integer in the vector. If the vector is empty, we want to return `None`. Otherwise, we want to return `Some` with the largest integer. Here's how we can implement this function in Rust:

```rust
fn largest(numbers: Vec<i32>) -> Option<i32> {
    if numbers.is_empty() {
        return None;
    }

    let mut largest = numbers[0];

    for num in numbers {
        if num > largest {
            largest = num;
        }
    }

    Some(largest)
}
```

Now we can use this function and use pattern matching to handle the both cases:

```rust
fn main() {
    let numbers = vec![1, 2, 3];

    match largest(numbers) {
        Some(num) => println!("Largest number: {}", num),
        None => println!("No largest number"),
    }
}
```

And this works!

![largest function works](<https://us-east-1.tixte.net/uploads/nexxel-has.no-friends.xyz/Alacritty-v0.10.0-portable_(1)_owuuiwwtmI.png>)

But we can do better. Rust provides an extensive standard library that includes a number of useful functions. Here we can create an iterator from our `Option` and use the `max` function to get the largest number:

```rust
fn largest(numbers: Vec<i32>) -> Option<i32> {
    numbers.into_iter().max()
}
```

This also automatically handles the case where the vector is empty, returning `None` for us. Rust's standard library is full of useful functions like this.

<br />

Want to return the double of the largest of the even numbers but only if it's less than 100? No problem!

```rust
fn largest_even_less_than_100(numbers: Vec<i32>) -> Option<i32> {
    numbers
        .into_iter() // create an iterator from the vector
        .filter(|num| num % 2 == 0) // filter out only even numbers
        .max() // get the largest number - returns an Option<i32>
        .map(|num| num * 2) // double the Some value inside the Option, leaves None unchanged
        .filter(|num| num < &100) // only return Some if the value is less than 100
}
```

I think you get the point.

<br />

### When Things Don't Go As Planned (Result)

<br />

Rust's `Result` type is another built-in abstraction that is often used to handle errors in Rust programs. It represents the success or failure of an operation. `Result` is an enum with two possible variants - `Ok` and `Err`. `Ok` represents the successful result of an operation, while `Err` represents an error that occurred during the operation. Here's how the `Result` type is defined in Rust:

```rust
enum Result<T, E> {
    Ok(T),
    Err(E),
}
```

You might be familiar with this pattern from other languages like Haskell's `Either` monad or OCaml's `result` type.

Let's look at an example of how `Result` can be used to handle errors. Let's say we have a function that takes two integers as arguments and returns the result of dividing the first integer by the second. However, division by zero is not allowed and will result in an error. We can use the `Result` type to handle the possible error case:

```rust
fn divide(x: i32, y: i32) -> Result<i32, &'static str> {
    if y == 0 {
        return Err("Cannot divide by zero");
    }
    Ok(x / y)
}
```

Now we can use this function and handle the success and error cases:

```rust
fn main() {
    match divide(10, 2) {
        Ok(result) => println!("Result: {}", result),
        Err(error) => println!("Error: {}", error),
    }
}
```

Pretty simple, right? Let's look at some functions Rust provides to make working with `Result` a breeze.

<br />

Say we wanted to add 10 to the result of a chained division operation. We could use nested `match` statements but that's a bit ugly and verbose. Rust has us covered with the `and_then` and `map` functions:

```rust
fn main() {
    let result = divide(10, 2)
        .and_then(|x| divide(x, 2))
        .map(|x| x + 10);

    match result {
        Ok(result) => println!("Result: {}", result),
        Err(error) => println!("Error: {}", error),
    }
}
```

This is a lot more concise than using nested `match` statements. Rust's standard library provides a lot more functions for working with `Result` and `Option`, so be sure to check them out.

<br />

## Vibrant Community and Ecosystem

<br />

Rust is more than just a language; it has a thriving ecosystem and community. This community is supported by a robust ecosystem of libraries, tools, and resources that make it easy to build, test, and deploy Rust applications.

<br />

Here are some key aspects of Rust's ecosystem and community:

<br />

- **Rustup** - Rustup is the official tool for installing and managing Rust. It makes it easy to install and update Rust and its associated tools. It also makes it easy to install and manage multiple versions of Rust on the same system.

- **Cargo** - Cargo is Rust's package manager. It's used to build, test, and run Rust applications. It also makes it easy to manage dependencies and avoid dependency hell. It also allows publishing libraries and binaries to [crates.io](https://crates.io), Rust's official package registry. It can also do benchmarks, and even manage multiple projects in the same repository using workspaces.

- **Libraries** - Rust has a large and growing collection of open-source libraries and frameworks that can be easily integrated into your projects. This includes everything from low-level system libraries to high-level web frameworks and game engines.

- **Tooling** - Rust has a strong focus on developer tools. Tools like Rustfmt, Clippy, and Rust Analyzer that help with code formatting, linting, and analysis.

- **Community** - The Rust community is known for being welcoming and supportive, with many resources available to help new users get started with the language. This includes online forums, chat rooms, and meetups, as well as a growing collection of Rust books and tutorials. I particurlarly love [Rust's community discord server](https://discord.gg/rust-lang-community). It's a great place to get help with Rust and meet other Rustaceans.

<br />

> These are some of my favourite features of Rust. If I go on, this article will be too long, so I'm linking some resources for Rust coolness below. I highly recommend checking them out.

<br />

## Some Other Cool Resources

<br />

- [How Cargo solved dependency hell](https://stephencoakley.com/2019/04/24/how-rust-solved-dependency-hell)
- [Rust Macros](https://youtu.be/MWRPYBoCEaY)
- [Rust's insanely helpful compiler](https://youtu.be/CJtvnepMVAU)
- [Rust traits](https://blog.rust-lang.org/2015/05/11/traits.html)