Python to Rust: Enum

August 25, 2017
11 min. read

This post is part of the Python to Rust series.

Enums are new with Python 3.4 and PEP 435, but have been backported. At first, I saw more trouble than benefit from Python Enums. They are typically used for type safety, and this isn’t really enforceable in Python. But, since they are a class, you can add additional functionality to them. This gives them more usefulness than I previously thought.

It isn’t easy to store data with them in most languages. Until I saw how Rust does this, I never thought I would want to. There are some ways to make Python Enums have some of the capabilities of a Rust Enum. Karl Kuczmarski has a good post covering some of this entitled Actually, Python enums are pretty OK.

We will be getting into quite a bit of code this time, and you will see how Rust separates definitions for Enum and Struct with implementation of methods. In many other languages, such as Python, these are defined together.

Direction Enum

Let’s make a simple Direction Enum. We will start with this in Python.

from enum import Enum


class Direction(Enum):
    North = 0
    East = 1
    South = 2
    West = 3

    def is_vertical(self):
        return self in (Direction.North, Direction.South)

    def is_horizontal(self):
        return self in (Direction.East, Direction.West)

    def to_vector(self):
        dirs = ((0, 1), (1, 0), (0, -1), (-1, 0))
        return dirs[self.value]

Lets do the same in Rust. This is a little longer, as I introduced tests for these are well. The next several code blocks are all the Rust source code. I’m breaking them into sections to discuss.

#[derive(PartialEq)]
enum Direction {
    North,
    East,
    South,
    West,
}

The derive statement is to allow Rust to implement a Trait automatically. This is possible when Enum or Structs are implemented with types that Rust knows how to handle. The PartialEq is required to use the equal == operator in code below. If we wanted to use the debug print {:?} we talked about in previous posts, we would include ,Debug after PartialEq.

Unlike Python, the Enum isn’t required to assign a value. You have the option of doing what is considered a “C-type” by just assigning each entry to a value.

impl Direction {
    fn is_vertical(&self) -> bool {
        if *self == Direction::North || *self == Direction::South {
            true
        } else {
            false
        }
    }

    fn is_horizontal(&self) -> bool {
        if *self == Direction::East || *self == Direction::West {
            true
        } else {
            false
        }
    }

    fn to_vector(&self) -> (i32, i32) {
        match *self {
            Direction::North => (0, 1),
            Direction::East => (1, 0),
            Direction::South => (0, -1),
            Direction::West => (-1, 0),
        }
    }
}

In Rust, the impl block (for implementation) is the functionality and is separate from the Enum or Struct. This seems a little different at first, but makes more sense as Traits are implemented in later posts. Instead of automatically deriving the Debug trait, we could have a block that starts with impl Debug for Direction and methods required to implement the trait. Then the {:?} debug print would display what we defined. Rusts traits based objects are similar to interfaces in C# or Java. This is different from the object inheritance Python offers.

Return type of a function is indicated after the ->. The main function we saw in earlier posts had no return. The first two functions indicate that we are returning bool and the last function is returning a tuple of two i32. I would make this a Vector struct in a real program, but I’m keeping this simple and similar to the Python version. And we have not yet covered structs yet.

All of our functions have no arguments, except for the reference (&) to self as &self. In Python, every argument is by reference. Rust is much more like C in that you can specify lower level control of value or reference passing. Rust can automatically dereference (point back at the object) when it can tell which is required. With comparisons, Rust doesn’t know if we want to compare the reference or the object. So we must manually dereference with the * operator, as seen in the *self == Direction::East. The boolean operators in Rust are the same as in C. So instead of the Python or we have ||.

The is_vertical and is_horizontal functions have a simple if else structure. However, there is no return. In Rust, if the line ends in a semicolon, it is a statement with no value. If there is no semicolon, it is an expression. So the true or false are expressions and become return values.

The to_vector method introduces a new control flow type of match. This is something like a switch, but with some powerful options. Rust requires that all cases are handled. I have every Direction as an option in the match. If this were not the case, this code would not compile. I’m again using the expression at the end of the matching Direction to automatically return the tuple.

One thing I should mention is that all of these functions and the Enum are currently private. If I tried to use these in a program that imports this library, nothing would be available to reference. I must include a pub keyword in front of the enum and fn to make them available.

While this is very simple and tests are a little over kill, I wanted to show how they are done in Rust.

#[cfg(test)]
mod tests {

    use Direction;

    #[test]
    fn is_vertical() {
        assert!(Direction::North.is_vertical());
        assert!(Direction::South.is_vertical());
        assert!(!Direction::East.is_vertical());
        assert!(!Direction::West.is_vertical());
    }

    #[test]
    fn is_horizontal() {
        assert!(!Direction::North.is_horizontal());
        assert!(!Direction::South.is_horizontal());
        assert!(Direction::East.is_horizontal());
        assert!(Direction::West.is_horizontal());
    }

    #[test]
    fn to_vector() {
        assert_eq!(Direction::North.to_vector(), (0, 1));
        assert_eq!(Direction::East.to_vector(), (1, 0));
        assert_eq!(Direction::South.to_vector(), (0, -1));
        assert_eq!(Direction::West.to_vector(), (-1, 0));
    }
}

This creates a separate module for tests. A module is the unit of organization for Rust. This is similar to a Python package. It isn’t required to put tests in a module, all that is required is the function have #[test] above it. However, using a module is good practice. The #[cfg(test)] is used to only compile this code if we are testing by running cargo test. This reduces the binary size of the final result and reduces compile time.

Since we made this a module, notice how we had to use Direction; at the top, to make Direction available inside the module.

Our tests are just using the assert! macros with ! negation and checking all 4 cases of both functions. The to_vector test is comparing equality with the expected tuple.

Enums with Values

The other thing I mentioned earlier is Rust’s ability to hold data in an Enum. Lets look at an Enum that might handle possible responses in a conversation.

enum Interaction {
    // Normal Enum
    DoNothing,
    // Tuple based values
    Say(&'static str),
    Shout(&'static str, u32),
    // Structure Based
    Physical { action: &'static str, leave_after: bool },
}

So we have DoNothing that requires no data, Say which has a str holding what to say, Shout with a str and a u32 for volume of shout, and a Physical response has named fields of action and leave_after.

fn describe_interaction(inter: Interaction) {
    match inter {
        Interaction::DoNothing => println!("Doing Nothing"),
        Interaction::Say(s) => println!("Say: '{}'", s),
        Interaction::Shout(s, v) => println!("Shout: '{}' at volume: {}", s, v),
        Interaction::Physical { action, leave_after: leave } => {
                println!("You {} ", action);
                if leave {
                    println!("And leave.");
                } else {
                    println!("And stay.");
                }
        },
    }
}

fn main() {
    let noth = Interaction::DoNothing;
    let say = Interaction::Say("Hello");
    let shout = Interaction::Shout("I SAID HELLO!", 12);
    let phys = Interaction::Physical { action: "Stomp Foot", leave_after: true };

    describe_interaction(noth);
    describe_interaction(say);
    describe_interaction(shout);
    describe_interaction(phys);
}

We will start from the bottom up. In main, we create 4 variables for the 4 types of Interaction Enum. Notice how the data is just added as part of the setting of the enums. We then call describe_interaction with each type. This method uses a match to identify a given enum and bind the data held with variables we can use.

The Say and Shout have unnamed values, that are assigned to s and v. These are just used with println! macro formatting. When getting data out of the named Physical data, I made use of a shortcut that doesn’t require you to name the parameter if the variable has the same name. So because action is our local variable and the name of the first piece of data, we don’t have to use action: action. Note that leave isn’t the same, so we need the full leave_after: leave.

The Physical match also displays the first complex scoped match we have seen. Everything in the curly braces is part of the match. This can get very complex and you can put as much as you want here. After a certain point, it is just unreadable. In this case, we are using the boolean leave, to determine what we print.

The following is the output from running this Rust code:

Doing Nothing
Say: 'Hello'
Shout: 'I SAID HELLO!' at volume: 12
You Stomp Foot 
And leave.

If you want to play with this code, here is a link to the Rust Playground with it loaded.

Special Enums

There are two special Enums built into Rust: Option and Result. In discussing these, we will need to bring up the idea of generics. This eliminates the need to rewrite the same code to handle multiple types, without losing the type safety. You can define data structures and functions with the idea of some type T.

Even though both of these are already defined, lets see what code is required to make them. I think this is important to understand how they work.

Option

The Option Enum is a way of handling a return that may or may not have a value. An example might be, give me the index of a vector that is equal some value. You will either have an index, or no value because you didn’t find it. Python would use None, and trust that you check for the value being None before using it. If you don’t, things blow up.

Since Rust requires all paths to be handled, you must handle a return that has Some value or None. If we look at the Option Enum, the code would look like this:

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

The T is using the generic capability of Rust to allow Some to hold the type defined when the Option is created. If we use Option<i32>, this would be the same as if the Some was defined like Some(i32). You could have a scalar, tuple, or more complex data type and it would work with Option. The idea is just that you have Some data or None.

fn get_optional_input() -> Option<i32> {
    if we_get_number_from_user {
        Some(number)
    } else {
        None
    }
}


fn main() {
    if let Some(num) = get_optional_input() {
        println!("We received {}", num);
    } else {
        println!("Nothing");
    }
}

The get_optional_input is psudo code that wouldn’t run, but gives you and idea of how you would make an Option return. In the main function, I’ve introduced a new assigment and control feature, the if let. This is true if the left side assignment can be made. Otherwise, the else clause will execute. This would be the same as the following match:

match get_optional_input() {
    Some(num) => println!("We received {}", num),
    _ => println!("Nothing"),
}

Note, I could have used None for the section expression, but used the _ catch all, because I had talked about it previously, but had not shown it before.

Result

Rust has the ability to panic, when really bad things happen. This is done with the panic! macro and is not recoverable. The idea is to stop execution and minimize damage, because we are in an unknown and unhandled situation.

There are no thrown and caught errors or exceptions, as is common in Python. The reason for this is that Rust wants to be absolutely sure you are handling all cases of execution. As Python programmers, we are often too lax in checking for exceptions. In Rust, it isn’t optional.

Error conditions are returned from functions. The Result type is used for this. The Result has two possibilities, like Option, but both have their own type. So this is using generics with two types. If we were to create it ourselves, the code would look like this:

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

The letters used with generics isn’t required to be anything, but by convention T is used for only one, as a shortcut for type. Here, T means type and E means error type.

Let’s make a divide function.

fn divide(num: i32, div: i32) -> Result<i32, &'static str> {
    if div == 0 {
        Err("You can't divide by zero.")
    } else {
        Ok(num / div)
    }
}

fn divide_display(num: i32, div: i32) {
    match divide(num, div) {
        Ok(val) => println!("{}/{} = {}", num, div, val),
        Err(err) => println!("{}", err),
    }
}

fn main() {
    divide_display(20, 10);
    divide_display(10, 0);
}

As you should expect, the output is:

20/10 = 2
You can't divide by zero.

Quicker Handling

There are some helper methods to get values out of Option and Result without full blown matches or ifs. For example, unwrap() will give you the Some value, but panic is None is returned. While not safe, you will find this is example code, as it is quick and doesn’t pull the focus away from what they are trying to show. A better use might be unwrap_or which allows you to specify a default if None was returned.

With Result you might not care about the value, but just want to know if it is_ok() or is_err(). The ok() method is often used in iterators when you only want to continue if it is a good result.

Look through the Option and Result documentation to see more functions and examples.


Part 4 of 4 in the Python to Rust series.

Series Start | Python to Rust: Types

comments powered by Disqus