Pattern matching + Enum proposals

Nice, for future consideration: “Control over layout and discriminators”:
I’m sure @nate might find an good use of this related to Optional and Niche :smiley:

Yep! I was about the raise the concern for not having “tuple-cases” but it seems like this will be supported which is great to see.
I believe this also helps quell @dmitry_salin 's concerns around the Rgb verbosity since you could then write the struct as such:

struct Rgb: ...

enum Color:
    case Red
    case Rgb(Rbg)

Exactly - “tuple-case” is a way to assign an alternative name to a type within the context of enum. In some cases this is redundant, but acceptable for the sake of simplifying the implementation of enums.

Hi all, long time no see. I have a lot of thoughts on enums, especially regarding pain points I’ve seen in other languages, and how Mojo might resolve them. For example, see this issue (pain point) in the Rust community. There’s a noteworthy comment from Niko Matsakis (Rust designer) in the replies:

We all agree that it is a common, and annoying, pattern in Rust today to have to make a struct for every enum variant and then just have the enum wrap those structs. This gives you the ability to have a “type for an enum variant”, but is annoying and inconvenient. So, for those reasons, we would love to see forward motion on this proposal. However, we also feel that this is striking at a fairly core part of the language, and there isn’t anyone on the team who has the bandwidth to actively liaison this effort.

Mojo has the opportunity to address these pain points right from the start, rather than trying to patch enums after they’ve shipped. So IMO it’s worthwhile treading carefully here, and pulling together the learnings from other language communities, before we commit to any particular design.

One direction worth exploring is whether Mojo actually needs a dedicated enum construct in the first place, or whether it’s sufficient to offer a new protocol on ordinary structs (a la Chris’s SumType trait), to enable structs to serve as the foundation for pattern matching.

Basically, I’m wondering whether we can evolve Variant (docs) into a fully capable “sum type” that supports pattern matching, and whether we can make it ergonomic enough to make enum declarations unnecessary. Python already has a nice vertical bar syntax that makes sum types easy to read and write (e.g. x: Int|None) and in Mojo this could desugar to Variant[Int, None], or something similar. A recursive list type could be declared simply as comptime LinkedList[T: Copyable] = None|Tuple[T, Box[LinkedList[T]]. This syntax happens to be more concise than Chris’s linked list enum posted earlier in this thread, but the primary benefit is that we didn’t need a dedicated enum syntax to define this type. All we need is Mojo’s existing type alias feature (and support for recursive aliases).

If we pursue this direction, when someone learning Mojo asks “how do I get sum types”, we can tell them “Mojo does things the Pythonic way, using (tagged) unions |”. So there’s a clean story we can tell; we wouldn’t be doing anything that’s weird/unfamiliar to the average programmer.

The last thing we’d probably want is a succinct syntax to declare zero-sized structs, to make simple enums like “red/green/blue” easy to write. Perhaps just being able to declare structs in bulk, e.g. struct Red, Green, Blue: pass would suffice. Or maybe a syntax like comptime Color = struct Red | struct Green | struct Blue.

My thoughts on this topic go a lot deeper. When I have time (probably this weekend), I’ll come back to this thread and write up a more detailed post. In the meantime, I’m keen to hear what people think about this design direction.

  1. I think it is possible to implement something special for a “sum type” that can be constructed from variadic parameter or parameter union expression like A | B | C.

  2. It is necessary to provide a mechanism for controlling alignment, trait conformance, etc., for a specific enum.

For example:

@align(64)
enum Color(Defaultable):
    case Red
    case Rgb(Rgb)

It seems that comptime declarations and a single Variant type cannot provide this.

It seems that comptime declarations and a single Variant type cannot provide this.

I don’t see any roadblocks. Here’s what I’d write, once struct extensions are implemented:

# Declares Color as an alias for Variant[Red, Green, Blue]
comptime Color = struct Red | struct Green | struct Blue

extension Color(Defaultable):
    def __init__(out self):
        self = Self(Red())   # delegates to Variant.__init__

This struct extension on Variant will be valid (i.e. satisfy the coherence rules) as long as Mojo adopts rules similar to those of Carbon. I posted info about Carbon’s rules here. (They were explained in a talk.)

Regarding alignment (@align): I’m not sure how it is modelled today, but I could imagine it also boiling down to a trait implementation.

Here’s another slight variation worth considering. What if this syntax:

enum Color:
    struct Red      # zero-sized struct with a default constructor
    struct Green
    struct Blue

Desugared to comptime Color = Enum[struct Red, struct Green, struct Blue].

So there is still only one core type for sum types (Enum, a.k.a. Variant), which can be defined in the standard library, and extended by way of extensions.

I’m still not convinced the enum sugar is actually necessary (| might be enough), but if we decide that it has real utility, we can still explore different options for what it desugars to. Chris’s proposal assumes that this syntax declares a brand new nominal struct, whereas I am wondering whether it should just desugar to an Enum/Variant struct.

If we still go for the current approach, then I don’t undestand why we’re deviating from current Mojo syntax at all.

why isn’t this

enum Color:
    case red
    case green
    case blue
    case rgb(r: Int, g: Int, b: Int)

just this?

struct Color(Enum): # or maybe we do keep the `enum` keyword
    var red: NoneType # or maybe we allow `...` here?
    var green: NoneType
    var blue: NoneType
    var rgb: Tuple[Int, Int, Int] # or `case rgb: RGB` if an RGB type exists

Something to also think about is that by making enum it’s own thing that gets auto-sintesized constructors, we’re kind of losing the control of what e.g. implicit constructors we allow for a given type. I hope we get that back in the future.


I’m generally +1 on Nick’s idea of improving Variant and folding enums into it.

The proposal has got some significant updates.

  • SumType is renamed to EnumLike
  • Any struct that conforms to EnumLikecan participate in pattern matching
  • The syntax for tuple cases has been reworked:
enum Color:
    case red
    case green
    case blue
    case rgb(Int, Int, Int)
  • imm as default convention
  • and much more, like easier struct cases:
enum Color:
    case red
    case green
    case blue
    case rgb(Rgb)

What are the advantages of using a single struct for all enums? In any case, different enums will be different types. However, there is a problem: decorators are applied to struct declarations, and something else applicable exclusively to such declarations might appear. Furthermore, changing anything in this single struct will affect all enums.

FWIW, if you didn’t read that far, the end of the proposal ponders a future direction where we support anonymous enums. It also points out a few of the concerns / open questions that would have to be resolved before doing so.

Let’s get the basic proposal implemented and landed and get experience with it, then we can assess further extensions.

-Chris

I generally prefer jumping over nested branching. I have not read the whole proposal since it’s a lot of text.

I hope mojo finds 2 ways to fix rust pattern matching:

A: borrow checker not playing nice with enums

B: needing ∞ match/if let sugar because control flow feels unnatural.