A Pleasing Symmetry in Rust

Appreciating how Rust enum variants are mirrors of its kinds of structs.

Assumed Audience: Software developers, especially who have an interest in (typed) programming languages.

I was mulling on some Rust and Swift code this morning, and it reminded me just how much I love a particular pleasing symmetry in Rust’s language design: specifically, how enum variants are mirrors of all the kinds of struct Rust supports.

There are three kinds of structs in Rust:

  • unit structs:

    struct Empty;
    
  • tuple structs:

    struct Wrapped(String);
    
    struct MultiWrappedGeneric<T, U, V>(T, U, V);
    
  • named field” structs (or regular” structs):

    struct Regular {
        some_field: Option<String>,
        another_field: u8,
    }
    
    struct RegularWithGeneric<T> {
        field: T,
    }
    

Each of these has its own uses, but for this post I’m going to leave aside the interesting bits about those and move right along to the thing which brought me here today: Rust’s enums have exactly the same set of varieties as its structs do.1 You can literally just copy the struct definitions into the body of an enum definition, move the generics up to the enum declaration itself, and everything just works”:

enum Example<T, U, V> {
    Empty,
    Wrapped(String),
    MultiWrappedGeneric(T, U, V),
    Regular {
        some_field: Option<String>,
        another_field: u8,
    },
    RegularWithGeneric {
        field: T,
    },
}

This is a small thing about the language, but it is a really nice small thing. It means that very often, there is no need to introduce additional type definitions for richer data structures. I always miss that when switching to Swift, because Swift’s enums only allow the unit” and tuple” varieties. The same code there would have both the struct and enum definitions:

struct Regular {
  var someField: String?
  var another_field: UInt8
}

struct RegularWithGeneric<T> {
  var field: T
}

enum Example<T, U, V> {
  case
    empty
  case
    wrapped(String)
  case
    multiWrappedGeneric(T, U, V)
  case
    regular(Regular)
  case
    regularWithGeneric(RegularWithGeneric<T>)
}

This isn’t just a matter of increased verbosity for authoring, either: It also affects access patterns for the structured data.2

Here’s accessing the data nested in RegularWithGeneric in Rust:

let an_example = Example::<String, String, String>::RegularWithGeneric {
    field: "Hello".into()
};

match an_example {
    Example::RegularWithGeneric { field } => println!("The field is {field}"),
    _ => println!("Skipping"),
}

And here’s the same in Swift:3

let an_example = Example<String, String, String>.regularWithGeneric(
  RegularWithGeneric(field: "Hello")
)

switch an_example {
case .regularWithGeneric(let wrapped):
  print("The field is \(wrapped.field)")
default:
  print("Skipping")
}

Intuitively, I find this somewhat surprising: In general, Swift prioritizes brevity much higher than Rust. On reflection, though, Rust also values symmetry higher than Swift does, so it makes some sense that the language design keeps this mirroring. That also isn’t a criticism of Swift here, and as far as I know there isn’t any principled reason why Swift couldn’t add this in the future — though I also haven’t checked to see if it has been proposed as part of the Swift Evolution process in the past.

These are the kind of small features from Rust I have come to appreciate a lot — the kinds of things I really miss working in TypeScript day by day.


  1. They also have one more variant, where a variant has a custom discriminant value, when (and only when) the enum has only the identifier/“unit struct form, like Empty. These are basically analogous to just defining a const value instead of a unit struct. ↩︎

  2. You can extend how pattern matching works by implementing ~= for your type — see this Swift by Sundell writeup for a great walkthrough — but this is what you get out of the box. ↩︎

  3. swiftformat and rustfmt make substantially different choices here, which exaggerates the differences a bit. ↩︎