15  Enum(eration)s

Objective

Understand what enums are and when you may want to use one.

Oftentimes a variable can only take on a small number of values. This is when an enumeration (enum) would be useful.

enums are values that can only take on a select few variants. They are defined using the enum keyword.

Enums in R

We use enums all the time in R and almost exclusively as function arguments.

The tidyverse design style guide has a great section on enums. See enumerate options.

args(cor)
function(
  x, y = NULL, use = "everything",
  method = c("pearson", "kendall", "spearman") # 👈🏼 enum!
)

I’ve written about this in more detail in my blog post Enums in R: towards type safe R

Example

For example, it may make sense to create an enum that specifies a possible shape.

enum Shape {
    Triangle,
    Rectangle,
    Pentagon,
    Hexagon,
}

Variant-specific behavior

The Shape enum can take on only one of those 4 variants. An enum is created using the format EnumName::Variant.

How do we actually determine behavior based on a variant? This is done using pattern matching.

The keyword match lets us perform an action based on an enum’s variant. It works by listing each variant and the behavior to that happens when that variant is matched.

The pattern match format uses the syntax Enum::Variant => action. When using match each variant much be enumerated:

=> is often referred to as “fat arrow.” But if you say “chompky” I’ll also get it.

match my_shape {
    Shape::Triangle => todo!(),
    Shape::Rectangle => todo!(),
    Shape::Pentagon => todo!(),
    Shape::Hexagon => todo!(),
}

todo!() is a placeholder that can be used to make the compiler happy

Example

For example we may want to print the number of verticies of a shape:

let my_shape = Shape::Triangle;

match my_shape {
    Shape::Triangle => println!("A triangle has 3 vertices"),
    Shape::Rectangle => println!("A rectangle has 4 vertices"),
    Shape::Pentagon => println!("A pentagon has 5 vertices"),
    Shape::Hexagon => println!("A hexagon has 6 vertices"),
}

Wildcard matching

Sometimes we want to customize behavior on only a subset of variants. We can use a catch all in the match statement _ => use the underscore to signify “everything else”.

match my_shape {
    Shape::Hexagon => println!("Hexagons are the bestagons"),
    _ => println!("Every other polygon is mid"),
}

Enums can impl, too

Enums can have methods too just like a struct using impl keyword

impl Shape {
    fn is_bestagon(&self) -> bool {
        match self {
            Self::Hexagon => true,
            _ => false
        }
    }
}

Exercise 1

  • Create an enum called Measure with two variants Euclidean and Haversine
  • Create a method called ndim() which returns 2 for Euclidean and 3 for Haversine
View solution
enum Measure {
    Euclidean,
    Haversine,
}

impl Measure {
    fn ndim(&self) -> i32 {
        match self {
            Self::Euclidean => 2,
            Self::Haversine => 3
        }
    }
}

Exercise 2

  • Create a new method distance() for Point struct that returns an f64
    • Arguments: &self, destination: &Self, measure: &Measure
  • When measure is Euclidean use the euclidean_distance() method
  • When the variant is Haversine use the haversine_distance() method

The haversine method is defined as:

Code for haversine_distance()
impl Point {
    fn haversine_distance(&self, destination: &Self) -> f64 {
        let radius = 6_371_008.7714; // Earth's mean radius in meters
        let theta1 = self.y.to_radians(); // Latitude of point 1
        let theta2 = destination.y.to_radians(); // Latitude of point 2
        let delta_theta = (destination.y - self.y).to_radians(); // Delta Latitude
        let delta_lambda = (destination.x - self.x).to_radians(); // Delta Longitude

        let a = (delta_theta / 2f64).sin().powi(2)
            + theta1.cos() * theta2.cos() * (delta_lambda / 2f64).sin().powi(2);

        2f64 * a.sqrt().asin() * radius
    }
}
View solution

impl Point {
    // Demonstrates using pattern matching an enum
    fn distance(&self, destination: &Self, measure: &Measure) -> f64 {
        match measure {
            Measure::Euclidean => self.euclidean_distance(destination),
            Measure::Haversine => self.haversine_distance(destination),
        }
    }
}