3 min read

Rust Book - Chapter 6.1

Defining an enum
💡
I've been looking into Rust for some time now, the following are my study notes on this chapter of the Rust book.

Definining an Enum

An Enum gives you a way of saying a value is one of a possible set of values. We might want to say that an Apple is one of a a set of possible fruits. An enum lets us encode these possibilities as an enum.
An enum value can only be one of its variants at any time.

An enum lets us enumerate all possible variants of a type. This is where it gets its name.

Take the example of an IP Address, which can be of either version four or six, but not both at the same time. We express this concept by defining an IpAddrKind enumeration, and listing the possible kinds of an address can be. These are the variants of the enum:

enum IpAddrKind {
    V4,
    V6,
}

IPAddrKind is now a custom data type we can use in our code.

Enum Values
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

The variants of the enum are namespaced under its identifier. We use a double colon to separate the two.

Enum can encode more information

Taking the IP address example, we can encode even more information about an IP Address like its address, as well as more information.

struct Ipv4Addr {}

struct Ipv6Addr {}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}

We can thus put any kind of data inside an enum variant: strings, numeric types, struct or even another enum. Enums can also define methods

Imagine we wanted to represent different types of Messages in a system, we can do this using structs like so:

struct QuitMessage; //unit struct
struct MoveMessage {x: i32, y:i32,}
struct WriteMessage(String); //tuple struct
struct ChangeColorMessage(i32, i32, i32); //tuple struct

This works, but we can't easily define a function to take any of these kinds of messages. We can express the same concept using an enum like so:

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

We can thus define a function to take a Message enum

fn handle(message_type: Messsage) {}

Option enum

The option type expresses the concept that a value could be something or nothing. Rust doesn't have the concept of null used by some languages, but it however encodes the same concept of a value being present or absent using the Option enum represented in the standard library like so:

enum Option<T> {
    None, //effectively same as null in other languages
    Some(T), //value is present and in the Some variant
}

Option<T> enum is so useful, it is included in prelude: you don't need to bring it into scope explicity. You can use its variants directly without the Option:: prefix

Since None value, in some sense means the same things as null in other programming languages: we don't have a valid value, why is having Option<T> better than having null? Because Option<T> and T (wher T can be any type) are different types, the compiler won't let us use an Option<T> value as if it were definitely a valid value.

fn main() {                     
    let x: i8 = 5;              
    let y: Option<i8> = Some(5);
                                
    let sum = x + y;            
}                               

if we run the code, we get the get the error:

error[E0277]: cannot add `Option<i8>` to `i8`
 --> src/main.rs:5:17
  |
5 |     let sum = x + y;
  |                 ^ no implementation for `i8 + Option<i8>`
  |
  = help: the trait `Add<Option<i8>>` is not implemented for `i8`
  = help: the following other types implement trait `Add<Rhs>`:
            <i8 as Add>
            <i8 as Add<&i8>>
            <&'a i8 as Add<i8>>
            <&i8 as Add<&i8>>

For more information about this error, try `rustc --explain E0277`.
error: could not compile `option_enum` (bin "option_enum") due to 1 previous error

The error means that Rust doesn't understand how to add an i8 and an Option<i8>, because they're different types. When we have a type like i8, the Rust compiler will check that we have a valid value. We can proceed confidently without having to check for null before using that value. Only when we have an Option<T> do we have to worry about the possibility of not having a value, and the compiler will also make sure that we handle that caseu before using the value. This work is offloaded to the compiler.

You have to convert an Option<T> to a T before you can perform T operations with it.