Nothing to it

Enumerations in Ribbon

March 14, 2025

Enums are a classic staple of programing languages. Ever since Algol I've heard. Enums with associated values are even better. Apparently introduced by Eiffel.

Here's enums in Ribbon:

let E = enum {
    VariantA(x: Int, y: String)
    VariantB
}

let e = E.VariantA(x: 32, y: "Hi")

match e {
    .VariantA(x, y) => print("VariantA: x=\(x) y=\(y)")
    .VariantB => print("VariantB")
}

And, as usual, some details:

But that's not all! I've implemented joining for enums!

let OkResult = enum {
    ResultPage(page: Page, nextPage: () -> OkResult)
    End
}

let ErrorResult = enum {
    NoInternet
    RateLimitExceeded
    InvalidCredentials
}

let Result = OkResult | ErrorResult

I haven't started implementing a standard library yet, but I imagine this will be a useful mechanism for encapsulating possible error types for a lot of IO methods.

Just to be clear, this does tolerate name collisions. For example:

let E1 = enum { A, B }
let E2 = enum { A, C }

let E = E1 | E2

E.B // Equivalent to E1.B
E.A // Not allowed, must be disambiguated as `E1.A` or `E2.A`

Associated values as structs (or tuples)

Enum variants with associated values are nothing more than structs. In last week's post, I described how to instantiate struct literals, but I didn't describe how to declare their types. That wasn't intentional, but I can remedy that for you now.

Here's four enum variants that are roughly equivalent:

enum {
    VariantA(x: Int, y: String) // Struct with type hints
    VariantB(x:, y:)            // Struct without type hints
    VariantC(:Int, :String)     // Tuple with type hints
    VariantD(:, :)              // Tuple without type hints
}

I know you must be wondering what happens without colons. I kind of struggled with this, since there are common uses of declaring a tuple with type hints, e.g. (Int, String) (like how Rust does it), and declaring a struct without type hints, e.g. (x, y). Many languages would disambiguate by having structs use {x, y}. But I'm not faltering, yet! I'm going with the safe approach of making it an error to not include colons. But we'll see if I have to change that.

What about enums values?

Yes, yes. Enums traditionally just have one value per variant. In fact, that's entirely how I've implemented them!

enum {
    VariantA(x: Int, y: String)
    VariantA = (x: Int, y: String) // same as above

    VariantB = 123

    VariantC       // Has a globally unique and undescribed value
}

Yes, that's right, the struct declaration (x: Int, y: String) is the variant's value! I'll have to make another post describing how I treat types as values, including struct/tuple types.

Now, it's probably not a good idea for some variants to be types while others are values. This can be fixed by putting a constraint on the values of the enum.

enum : Int {
    VariantA = 1
    VariantB = 2
    VariantC     // Not allowed! Enum variants do not get implicit integer values!
}

Also, you may have noticed that unlike most languages, Ribbon enums are usually not named. Well, you can name them yourself, using a variable, as in let E = enum {...}. This is another aspect of types being values. However, I believe I can compromise and also allow enum names to be specified in the enum expression, like other languages.

enum E { ... }

// identical to

let E = enum { ... }

Now, don't get any funny ideas! Just because enum types are not named doesn't mean they can be confused for each other. Each enum {...} creation can be thought of as having a globally unique (and undescribed) discriminant, which means they never collide.

let E = enum {
    VariantA
}

let e = {
    // New scope! This shadows the previous enum E
    let E = enum {
        VariantA
    }

    E.VariantA // The return value of this block, which stores into `e`
}

print(e == E.VariantA) // Prints false!

Also, a bonus. Since enum blocks can just be declared on the fly, you can do cool things for prototyping! For example...

let some_function(...) -> enum {
    StatusA
    StatusB
} = {
    ...

    .StatusB // This is the return value again
}

Pretty nice, huh! This is a feature I wanted in other languages for so long, and now it's finally here!

Conclusion

So, that's Ribbon's enums! I hope you found this fascinating! I'm still not sure what topic I'll present to you next week, but you can definitely expect another post. I might discuss types as values and how it fits into templates, or maybe I'll share some interesting implementation details of my interpreter written in Rust. Regardless, until then, take care!