rust-enums-tipos-algebraicos

Enums in Rust: Algebraic Types and Option Handling

  • 4 min

An enum in Rust is a type that can represent one of several possible variants.

If you come from C, C#, or Java, you probably think of an enum as a nice way to assign names to integer values.

  • Color.Red is 0.
  • Color.Green is 1.

It’s useful for state machines or configurations, but not much else.

In Rust, enums are a completely different beast. Rust adopts the concept of Algebraic Data Types (ADTs) from functional languages (like Haskell).

This means that an enum in Rust doesn’t just tell us “which variant it is,” but each variant can contain different data inside it.

Simple Enums

Let’s start with the basics, what you already know. An Enum allows us to define a type that can be one of several possible variants.

enum Direction {
    North,
    South,
    East,
    West,
}

fn main() {
    let direction = Direction::North;
}
Copied!

Nothing new so far. We can use it to add clarity to code instead of using magic numbers (0, 1, 2, 3).

Enums with Data

Here’s where it gets interesting. In Rust, we don’t need to create a separate struct to associate data with a variant. We can store data directly inside the enum variant.

Imagine we want to model IP addresses. We have two standards: V4 and V6.

  • A V4 address is four 8-bit numbers (e.g., 127.0.0.1).
  • A V6 address is a long String.

In C# or Java, we would have to create a base class IpAddr and two subclasses, or a struct with optional fields. In Rust, we model it like this:

enum IpAddr {
    V4(u8, u8, u8, u8), // This variant stores 4 bytes
    V6(String),         // This variant stores a String
}

fn main() {
    let home = IpAddr::V4(127, 0, 0, 1);
    let loopback = IpAddr::V6(String::from("::1"));
}
Copied!

Notice how powerful this is:

  1. IpAddr::V4 and IpAddr::V6 are of the same type (IpAddr). We can pass either of them to a function that expects an IP.
  2. Each variant stores completely different data types.

A Message System

To see the real flexibility, look at this example of an enum that defines the messages that can be sent in an application:

enum Message {
    Quit,                       // No associated data (like a C enum)
    Move { x: i32, y: i32 },    // Has named fields (like a Struct)
    Write(String),              // Has a single String (like a Tuple Struct)
    ChangeColor(i32, i32, i32), // Has three integers (like a Tuple)
}
Copied!

If we wanted to model this only with structs, we would need four different types and some additional mechanism to treat them as “any message.” With the enum, everything is grouped under the Message type.

Methods on Enums

Just like Structs, Enums can also have impl blocks to define methods.

impl Message {
    fn execute(&self) {
        // Logic would go here (using match, which we'll see in the next article)
    }
}

fn main() {
    let m = Message::Write(String::from("Hello"));
    m.execute();
}
Copied!

The Option Enum

In most languages, there is the concept of null (or nil, None). It’s the “billion-dollar mistake” (the costliest error in history) because trying to use a null value makes the program crash (NullReferenceException).

Ordinary types and references in Rust do not use null as a valid value. Raw pointers can be null, especially when interoperating with other languages, but they fall outside the usual guarantees of references.

So how do we express that something might not exist? (For example, searching for a user in a database and not finding them).

Rust uses a generic Enum included in the standard library called Option.

enum Option<T> {
    None,    // Represents the absence of a value (like null)
    Some(T), // Represents that there IS a value of type T
}
Copied!

This enum is so important that Rust imports it automatically (you don’t need to write Option::Some, just Some).

Why is Option better than null?

Imagine you have an i32 number. In Rust, an i32 is ALWAYS a valid number. It can never be null. The compiler guarantees that you can do math with it safely.

If you want a number that could not exist, you must use Option<i32>.

let number: i32 = 5;
let optional_number: Option<i32> = Some(5);
let empty_number: Option<i32> = None;

// let sum = number + optional_number; // ❌ ERROR!
Copied!

The compiler won’t let you add an i32 to an Option<i32>. Why? Because optional_number could be None. Rust forces you to check before using it whether there is a value or not.

This forces you to represent and handle absence explicitly. We can still cause a panic if we use unwrap() on None, but we cannot simply confuse an Option<T> with a T.