Rusticity: convert an integer to an enum

I am learning how to program in Rust. Thinking in Rust is a delightful experience and the more I practice Rust the more I feel how it empowers developers to solve complex problems with confidence.

However, I sometimes get frustrated on my way to rusticity. For instance, when a programming task easily done in C or Python requires more work in Rust. I experienced this when trying to convert an integer to an enum. This blog post compares how this is usually done in C and how to do it safely in Rust.

Converting an integer to an enum in C

In C, the enumeration constants have the type int. Thus, an integer value can be directly assigned to an enum.

#include <stdio.h>

enum atomic_number {
    HYDROGEN = 1,
    HELIUM = 2,
    // ...
    IRON = 26,
};

int main(void)
{
    enum atomic_number element = 26;

    if (element == IRON) {
        printf("Beware of Rust!\n");
    }

    return 0;
}

While it is easy to assign an integer value to an enum, the C compiler performs no bounds checking. Nothing prevents us from assigning an impossible value to an atomic_number enum.

Converting an integer to an enum in Rust, the naïve way

Let's write a similar program in Rust:

enum AtomicNumber {
    HYDROGEN = 1,
    HELIUM = 2,
    // ...
    IRON = 26,
}

fn main() {
    let element: AtomicNumber = 26;
}

When we try to compile and run the program with cargo run, the Rust compiler reports a mismatched types error:

error[E0308]: mismatched types
 --> src/main.rs:9:33
  |
9 |     let element: AtomicNumber = 26;
  |                  ------------   ^^ expected enum `AtomicNumber`, found integer
  |                  |
  |                  expected due to this

The compiler error clearly indicates that AtomicNumber and integer are two different types.

To explicitly convert an integer to our AtomicNumber enum, we can write a conversion function that takes an unsigned 32-bits integer as parameter and returns an AtomicNumber.

enum AtomicNumber {
    HYDROGEN = 1,
    HELIUM = 2,
    // ...
    IRON = 26,
}

impl AtomicNumber {
    fn from_u32(value: u32) -> AtomicNumber {
        match value {
            1 => AtomicNumber::HYDROGEN,
            2 => AtomicNumber::HELIUM,
            // ...
            26 => AtomicNumber::IRON,
            _ => panic!("Unknown value: {}", value),
        }
    }
}

fn main() {
    let element = AtomicNumber::from_u32(26);
}

The from_u32() function is an associated function of the AtomicNumber type because it is associated with that type, and unlike a method does not take a first parameter named self.

There are several issues in from_u32():

  • When the given value does not match any variant in the enumeration, panic!() terminates the program.
  • Both the enumeration definition and the conversion function match enumeration variants with integers. This duplication is error-prone.
  • The conversion function grows with the number of variants.

The periodic table contains more than 100 elements. Implementing a conversion function for atomic numbers is boring and error-prone. There is a better way.

Converting an integer to an enum in Rust with num-derive

A more elegant solution is to use the FromPrimitive trait from the num crate coupled with syntax extensions from the num-derive crate.

In Cargo.toml, add dependencies for num, num-derive, and num-traits:

[dependencies]
num = "0.4"
num-derive = "0.4"
num-traits = "0.2"

Then, use the #[derive] attribute:

extern crate num;
#[macro_use]
extern crate num_derive;

#[derive(FromPrimitive)]
enum AtomicNumber {
    HYDROGEN = 1,
    HELIUM = 2,
    // ...
    IRON = 26,
}

fn main() {
    let element = num::FromPrimitive::from_u32(26);
    match element {
        Some(AtomicNumber::IRON) => println!("Beware of Rust!"),
        Some(_) => {},
        None => println!("Unknown atomic number")
    }
}

The #[derive(FromPrimitive)] attribute instructs the Rust compiler to generate a basic implementation of the FromPrimitive trait for the AtomicNumber enumeration. Unlike our handwritten from_u32() function, the conversion function generated by the Rust compiler returns an Option which is either Some atomic number or None if the given integer does not match any known atomic number. This is much safer than calling panic!().

With the #[derive(FromPrimitive)] attribute our Rust program is nearly as concise as the same program written in C with the bonus of being safer.

Harder, better, safer, rustier

While I had some hard time figuring how to convert an integer value to an enum variant in Rust, I feel reassured by its type safety and pleased by its ecosystem of crates.

Rust on the pont de Bir-Hakeim

Rust on the pont de Bir-Hakeim