Enumerations in Ribbon
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:
- Enum variants are their own type. For example, a function that takes only an
E.VariantA
would have a signature like(x: E.VariantA) -> ()
. Calling it with anE.VariantB
would yield an error. This is similar to failing an arbitrary constraint, where the function call as a whole would return an error value. It would be implemented as a runtime check in the general case, but this can be optimized during static analysis if the argument is known to always be anE.VariantA
. - Enum variant literals don't need to explicitly state which enum they belong to. A variant
without an enum name can be constructed and implicitly converted, such as in:
let e: E = .VariantA(x: 32, y: "Hi")
. - In fact, the enum name being optional has some interesting implications. From the sample above,
we can delete all mentions of
E
, including the entire enum declaration. This is not a good practice, but it certainly does make prototyping fast. Here's what it would look like:let e = .VariantA(x: 32, y: "Hi") match e { .VariantA(x, y) => print("VariantA: x=\(x) y=\(y)") .VariantB => print("VariantB") }
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!