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. This happened to me not so long ago when I had to convert an integer to an enum. Let's see how this is usually done in C and how this can be done 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 an equivalent 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 defined only in the context of this 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, the execution is aborted with panic!().
  • The integer value for each enumeration variant is duplicated in the enumeration definition and in the conversion function. We must take care to use the same value in both locations.
  • If the enumeration contains a large number of variants, the conversion function becomes very long.

Since there are more than 100 atomic numbers, implementing a conversion function quickly becomes boring. There should be 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 equivalent 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 with how its ecosystem of crates can simplify my work as a programmer.

Rust on the pont de Bir-Hakeim

Rust on the pont de Bir-Hakeim