Picking Variants or Fields Out Of Collections of Enums

Ryan James Spencer

June 10 2020, 8:06PM

Occasionally you want to pull out a specific field or variant out of one or more enums. Doing this with pattern matching is tedious and verbose, but there's a simple way to use methods and combinators to do exactly what you want.

There's a pattern I use to solve both of these. If you already have the collection in hand, you can write a simple method that operates on the type named something like as_foo where foo is the name of the variant or field name you are after. There's a clippy lint that says as_* functions should always take references should I've followed the lint in the following example but it doesn't really matter what you name the method and it's fine to have the method take ownership of the value, too, if that makes sense for your use case. When you define a method you can use it either with the basic method syntax value.as_foo() or you can access it as an associated function e.g. Type::as_foo(value). Then we can use either method in tandem with the filter_map or flat_map methods of an iterator. I personally prefer the more terse way of passing the associated function instead of the closure, which is somtime referred to as "point free" style where the arguments, or points, are not mentioned:

#[derive(Debug, Clone, PartialEq)]
enum E {
    A { x: i32 },
    B { x: i32 },
}

impl E {
    pub fn as_x(&self) -> Option<i32> {
        // might need to rearrange this
        // if a variant is added that
        // does not include x.
        Some(match self {
            E::A { x } => *x,
            E::B { x } => *x,
        })
    }

    pub fn as_b(&self) -> Option<&E> {
        match self {
            x @ E::B { .. } => Some(x),
            E::A { .. } => None,
        }
    }
}

pub fn main() {
    // method access off type.
    let a = E::A { x: 1 };
    let b = E::B { x: 2 };
    assert_eq!(a.as_x(), Some(1));
    assert_eq!(b.as_x(), Some(2));

    // associated function on impl.
    let a = E::A { x: 1 };
    let b = E::B { x: 2 };
    assert_eq!(E::as_x(&a), Some(1));
    assert_eq!(E::as_x(&b), Some(2));

    // in a collection.
    let a = E::A { x: 1 };
    let b = E::B { x: 2 };
    let as_and_bs = vec![a, b];
    let xs = as_and_bs.iter().filter_map(E::as_x).collect::<Vec<i32>>();
    assert_eq!(xs, vec![1, 2]);

    // selecting a field as a dummy pattern match.
    let b = E::B { x: 2 };
    let xs = Some(&b).and_then(E::as_b);
    assert_eq!(xs, Some(&E::B { x: 2 }));
}

Playground.

Pattern matching is powerful but sometimes you can reduce the number of explicit pattern matches you perform by taking advantage of functions and combinators and keeping the logic small and simple, letting you reason about what the result ought to be on the other end. In the last case using and_then above, we can reason that whenever we call as_b we're sure to get a single pattern match if we must simply checking for Some(E::B { .. }) or None. The compiler may not understand that, though, and you'll most likely have to include a wildcard case, but the brilliance of combinators is that you can chain them together in a pipeline similarly to the fluid interface that iterators present.


Join the Newsletter