├── Makefile ├── README.md ├── c ├── .gitignore └── main.c ├── first-proposal.md ├── java ├── .gitignore └── enum.java ├── python └── enumtest.py ├── rfc.md └── rust ├── .gitignore ├── Cargo.lock ├── Cargo.toml └── src └── main.rs /Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: c java python rust 3 | 4 | .PHONY: c java python rust 5 | 6 | clean: clean-c clean-java clean-rust 7 | 8 | c: c/main.c 9 | cd c && \ 10 | gcc main.c && \ 11 | ./a.out 12 | 13 | clean-c: 14 | rm -f c/a.out 15 | 16 | java: 17 | cd java && \ 18 | javac enum.java && \ 19 | java Main.class 20 | 21 | 22 | clean-java: 23 | cd java && rm -f *.class 24 | 25 | python: 26 | cd python && \ 27 | python3 enumtest.py 28 | 29 | rust: 30 | cd rust && \ 31 | cargo run 32 | 33 | clean-rust: 34 | cd rust && \ 35 | cargo clean -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A survey of programming language enum support 2 | 3 | ## Introduction 4 | 5 | As of mid-2020, there is some discussion of adding enumerations (enums) to PHP. There are many good reasons to do so, most around enabling better data modeling and type checking, but that doesn't suggest how to do it. Enumerations in practice refer to a very wide range of functionality depending on the language, from barely above constants to a core part of the type system. 6 | 7 | As I am wont to do, I decided the best thing to do would be to survey the existing marketplace and see what other languages did, and what we can steal outright. (As the saying goes, "PHP evolves by beating up other languages in dark alleys and going through their pockets for loose syntax.") I therefore looked at 12 different languages with some kind of native enumeration support. The survey below is intended as a reasonably fair overview and summary of the available languages. My own thoughts and analysis are included at the end. For some languages I have included runnable sample code in the appropriate subdirectory. Whether or not there is sample code depends primarily on whether I had a runtime for the language already installed. 8 | 9 | I deliberately excluded languages with no native enum support. Languages such as Javascript, Go, or Ruby do not (as far as I can tell) have any native enumerations, although there are various hacky ways to simulate them in user space. That is not of interest to us at this time. 10 | 11 | If you spot any errors in the survey below, please let me know. 12 | 13 | ## Survey 14 | 15 | - [C](#c) 16 | - [C++](#c-1) 17 | - [Java](#java) 18 | - [Python](#python) 19 | - [Typescript](#typescript) 20 | - [Haskell](#haskell) 21 | - [F#](#f) 22 | - [C#](#c-2) 23 | - [Swift](#swift) 24 | - [Rust](#rust) 25 | - [Kotlin](#kotlin) 26 | - [Scala](#scala) 27 | 28 | ### C 29 | 30 | In C, enumerations are really just a wrapper for named integer constants. They are defined with the keyword `enum`, usually in combination with `typedef`. For example: 31 | 32 | ```c 33 | #include 34 | 35 | typedef enum { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday } Day; 36 | 37 | void printer(Day d) { 38 | printf("The day is: %d\n", d); 39 | } 40 | 41 | int main(void) { 42 | Day d = Tuesday; 43 | 44 | printer(d); 45 | printer(4); 46 | return 0; 47 | } 48 | ``` 49 | 50 | Even though `printer()` takes a `Day` parameter, passing an integer literal works fine. If no integer value for an enum element is specified, the compiler assigns one automatically starting from 0. So `Monday` is 0, `Tuesday` is 1, etc. You can specify an equivalent integer for an enum value, including making multiple values refer to the same integer: 51 | 52 | ```c 53 | typedef enum { 54 | Working, 55 | Failed = 5, 56 | Busted = 5; 57 | } Status; 58 | ``` 59 | 60 | Note that even though `d` is of `Day` type, the enum constant `Tuesday` is defined in the **global** scope. That is, the following code does not compile, as `Monday` is defined twice. 61 | 62 | ```c 63 | typedef enum { Tuesday = 1, Monday, Wednesday } WeirdDays; 64 | typedef enum { Monday, Tuesday, Wednesday } Day; 65 | ``` 66 | 67 | ### C++ 68 | 69 | C++ is backwards-compatible with C, so the [previous section](#c) applies. In addition, starting with C++11, scoped enumerations (defined with `enum struct` or `enum class`) have been introduced. 70 | 71 | The enums are defined the same way as in C (so individual enumerators' values can be specified, etc.). There is no automatic conversion from the scoped enum type to the underlying integer type. 72 | 73 | *Note:* Even though the defining keywords are `enum struct`, the type itself does not behave like a `struct`: no fields or member methods can be defined. 74 | 75 | ```cpp 76 | #include 77 | 78 | typedef enum { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday } Day; 79 | enum struct ScopedDay { Monday = 9, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday }; 80 | 81 | void printer(Day d) { 82 | std::cout << "The classical day is " << d << '\n'; 83 | } 84 | 85 | void printer(ScopedDay d) { 86 | std::cout << "The scoped day is " << static_cast(d) << '\n'; 87 | } 88 | 89 | int main() { 90 | Day d1 = Tuesday; 91 | ScopedDay d2 = ScopedDay::Tuesday; 92 | 93 | printer(d1); // 1 94 | printer(d2); // 10 95 | return 0; 96 | } 97 | ``` 98 | 99 | ### Java 100 | 101 | In Java, enums are, unsurprisingly, a shorthand for classes with class constants. They can be defined standalone or within a class, since Java supports inner classes. As a result, enums can support arbitrary methods. The specific values can map to internal integer values, or they can be auto-assigned by the compiler. 102 | 103 | The simple case looks like this: 104 | 105 | ```java 106 | enum Suit { 107 | HEARTS, 108 | DIAMONDS, 109 | CLUBS, 110 | SPADES 111 | } 112 | ``` 113 | 114 | Enum values have a number of methods on them by default to access metadata, including `Suit.valueOf("HEARTS")` (returns "HEARTS") and `Suit.valueOf("HEARTS").ordinal()` (returns 0). 115 | 116 | The values of an enum can be iterated as a set: 117 | 118 | ```java 119 | for (Suit s : Suit.values()){ 120 | System.out.println(s); 121 | } 122 | ``` 123 | 124 | Because they're built on classes, enums can have methods. 125 | 126 | ```java 127 | enum Suit { 128 | HEARTS, 129 | DIAMONDS, 130 | CLUBS, 131 | SPADES; 132 | 133 | public String color() { 134 | switch (this) { 135 | case SPADES: 136 | return "Swords of a soldier"; 137 | case CLUBS: 138 | return "Weapons of war"; 139 | case DIAMONDS: 140 | return "Money for this art"; 141 | default: 142 | return "Shape of my heart"; 143 | } 144 | } 145 | } 146 | ``` 147 | 148 | The switch statement is not exhaustive on enums, however. 149 | 150 | Enum constructors are always private, so they can only be called from the definition of enum members. They also support interfaces. 151 | 152 | ```java 153 | enum Suit { 154 | HEARTS("H"), 155 | DIAMONDS("D"), 156 | CLUBS("C"), 157 | SPADES("S"); 158 | 159 | private String abbrev; 160 | 161 | Suit(String abbrev) { 162 | this.abbrev = abbrev; 163 | } 164 | 165 | String shortName() { 166 | return abbrev; 167 | } 168 | } 169 | ``` 170 | 171 | Further reading: https://www.javatpoint.com/enum-in-java 172 | 173 | ### Python 174 | 175 | Python builds its enum support on top of classes. An "enum class" is simply a class that extends the `enum.Enum` parent, which has a lot of methods pre-implemented to provide Enum-ish behavior. All properties of the class are enum members: 176 | 177 | ```python 178 | import enum 179 | 180 | class Suit(enum.Enum): 181 | HEARTS = enum.auto() 182 | DIAMONDS = enum.auto() 183 | CLUBS = 'C' 184 | SPADES = "S" 185 | ``` 186 | 187 | Enum members can be any int or string primitive, or can be auto-generated. The auto-generation logic can also be overridden by defining a `_generate_next_value_()` method in the class. When an enum value is cast to a string, it always shows as `Card.CLUBS` or similar, but can be overridden by implementing the `__str__` method. 188 | 189 | Enum member names must be unique, but values need not be. If two members have the same value then the syntactically first one wins, and all others are alises to it. The aliases will be skipped when iterating an enum or casting it to a list. If needed, you can get the original list with `Card.__members__.items()`. 190 | 191 | As a class, an enum can also have methods. However, the methods have no native way to vary depending on which enum value they're on. You can check the value within the method, though: 192 | 193 | ```python 194 | class Suit(enum.Enum): 195 | HEARTS = enum.auto() 196 | DIAMONDS = enum.auto() 197 | CLUBS = 'C' 198 | SPADES = "S" 199 | 200 | def color(self): 201 | if self in [self.CLUBS, self.SPADES]: 202 | return "Black" 203 | else: 204 | return "Red" 205 | ``` 206 | 207 | Because Python lacks any meaningful type declarations on variables, parameters, or return values, there's no way to restrict a value to an enum list. Enum classes also cannot be extended. 208 | 209 | The `Enum` class also has an alternate function-style syntax for simple cases: 210 | 211 | ```python 212 | Suit = Enum('Suit', 'HEARTS DIAMONDS CLUBS SPADES') 213 | ``` 214 | 215 | Further reading: https://docs.python.org/3/library/enum.html 216 | 217 | ### Typescript 218 | 219 | Typescript supports primitive enumerations, including both constant and runtime-defined values. Depending on the details they may or may not get compiled away to literal constants in code. It has its own dedicated keyword. 220 | 221 | ```typescript 222 | enum Suit { 223 | Hearts, 224 | Diamonds, 225 | Clubs, 226 | Spades, 227 | } 228 | ``` 229 | 230 | is equivalent to 231 | 232 | ```typescript 233 | enum Suit { 234 | Hearts = 0, 235 | Diamonds = 1, 236 | Clubs = 2, 237 | Spades = 3, 238 | } 239 | ``` 240 | 241 | Enums can also have string values if specified explicitly. Values can be set based on some other value, even function definitions: 242 | 243 | ```typescript 244 | enum FileAccess { 245 | // constant members 246 | None, 247 | Read = 1 << 1, 248 | Write = 1 << 2, 249 | ReadWrite = Read | Write, 250 | // computed members 251 | UserSetting = userDefaultValue() 252 | } 253 | ``` 254 | 255 | Normally enums exist at runtime, but a fully-constant enum can also be flagged to compile-away to raw constants in the source code: 256 | 257 | ```typescript 258 | const enum ShouldWe { 259 | No, 260 | Yes, 261 | } 262 | ``` 263 | 264 | Enum types can be used as type declarations: 265 | 266 | ```typescript 267 | function pickCard(desiredSuit: Suit): Card { } 268 | ``` 269 | 270 | Further reading: https://www.typescriptlang.org/docs/handbook/enums.html 271 | 272 | ### Haskell 273 | 274 | Strictly speaking Haskell doesn't have enums, but the way its type system works gives you something close enough that I'm going to include it. In Haskell, you define a new data type with the `data` keyword, which can be defined in terms of other data types and type constructors. 275 | 276 | It's really hard to explain without going into the whole type system, so I'll stick to some examples: 277 | 278 | ```haskell 279 | data Suit = Hearts | Diamonds | Clubs | Spades 280 | ``` 281 | 282 | The type "Suit" has only four values, one for each suit. They are not backed by a primitive value but literally are those values only. Haskell doesn't have methods as we'd understand them in the OOP world, and I've not been able to wrap my brain around Haskell enough to say if you can attach methods consistently to types of an Enum. They can, however, be used in pattern matching: 283 | 284 | ```haskell 285 | data Color = Red | Black 286 | 287 | suitColor :: Suit -> Color 288 | suitColor Hearts = Red 289 | suitColor Diamonds = Red 290 | suitColor _ = Black 291 | ``` 292 | 293 | Because type values are technically not values but "type constructors" they can be parameterized by other values. For instance, the infamous Maybe Monad is defined as: 294 | 295 | ```haskell 296 | data Maybe a = Just a | Nothing 297 | ``` 298 | 299 | That is, a "Maybe" can be either the literal `Nothing` or a `Just` combined with some other value, which can then be extracted later using pattern matching. 300 | 301 | ```haskell 302 | stuff :: Maybe a -> Int 303 | stuff Nothing = 0 304 | stuff Just a = a 305 | ``` 306 | 307 | Further reading: https://wiki.haskell.org/Type 308 | 309 | ### F# 310 | 311 | F#, in what seems to be a very on-brand move, has both union types *and* enums. They are very similar but not quite the same thing. 312 | 313 | Union types in F# look and act an awful lot like Haskell, including the requirement that the unioned types start with a capital. 314 | 315 | ```f# 316 | type SuitUnion = Hearts | Diamonds | Clubs | Spades 317 | ``` 318 | 319 | They have no underlying primitive equivalent. F#'s `match` directive forces you to enumerate all possible values, to help avoid errors: 320 | 321 | ```f# 322 | let color = match x with 323 | | Hearts -> Red 324 | | Diamonds -> Red 325 | | Clubs -> Black 326 | | Spades -> Black 327 | ``` 328 | 329 | Enums in F#, by contrast, are backed by underlying integer primitives that you specify. Strings are not allowed. They can be all lowercase if you want, but have to be qualified when referencing to them: 330 | 331 | ```f# 332 | type SuitEnum = Hearts = 1 | diamonds = 2 | Clubs = 3 | Spades = 4 333 | 334 | let color = match x with 335 | | SuitEnum.Hearts -> "Red" 336 | | SuitEnum.diamonds -> "Red" 337 | | SuitEnum.Clubs -> "Black" 338 | | SuitEnum.Spades -> "Black" 339 | | _ -> "What kind of deck are you using?" 340 | ``` 341 | 342 | Enums can be cast to and from integers. That also, oddly, allow you to define an enum value that is out of range. 343 | 344 | ```f# 345 | // This is, amazingly, legal. 346 | let horseshoe = enum(5) 347 | ``` 348 | 349 | For that reason, the `_` fallback match arm is required for enums, but not for unions. 350 | 351 | Because F# doesn't have function parameter or return types, neither unions nor enums can be type defined in a function signature. 352 | 353 | Further reading: https://fsharpforfunandprofit.com/posts/enum-types/ 354 | 355 | ### C# 356 | 357 | C# enums are explicitly just named integer constants, much like in C. They can be defined within a class like constants, or (I think) stand-alone with a namespace. 358 | 359 | ```csharp 360 | enum Suits 361 | { 362 | Hearts = 0, 363 | Diamonds, 364 | Clubs, 365 | Spades 366 | } 367 | ``` 368 | 369 | If a value is not specified, it will be set to the highest existing value + 1. 0 is the default first value but you can set your own. They are referenced scoped, so `Suits.Diamonds`, `Suits.Spades`, etc. 370 | 371 | Values can also be defined based on other enum values, bitmask style, such as `RedCards = Hearts|Diamonds`. However, that only works if the explicit values are defined as bit flags. 372 | 373 | Enums need to be cast to an integer explicitly in order to use as an int. 374 | 375 | ```csharp 376 | Console.WriteLine((int)WeekDays.Monday); 377 | ``` 378 | 379 | An `Enum` class contains various static methods for manipulating enumerations further. For instance, to get a list of the names in a given enumeration: 380 | 381 | ```csharp 382 | foreach (string str in Enum.GetNames(typeof(WeekDays))) { 383 | Console.WriteLine(str); 384 | } 385 | ``` 386 | 387 | Or this somewhat crazy way to cast an integer up to an enum member: 388 | 389 | ```csharp 390 | WeekDays wdEnum; 391 | Enum.TryParse("1", out wdEnum); 392 | Console.WriteLine(wdEnum); 393 | ``` 394 | 395 | Although they're not a class, you can technically add "extension methods" to enums that end up looking kind of like them. For instance: 396 | 397 | ```csharp 398 | public static string Color(this Suit s) { 399 | switch (s) 400 | { 401 | case Hearts: return "Red"; 402 | case Diamonds: return "Red"; 403 | case Clubs: return "Black"; 404 | case Spades: return "Black"; 405 | } 406 | } 407 | 408 | var theColor = Suit.Clubs.Color(); 409 | ``` 410 | 411 | Further reading: https://www.tutorialsteacher.com/csharp/csharp-enum 412 | 413 | ### Swift 414 | 415 | Swift's enumerations are closer to union types, but still called enumerations. (Go figure.) They form a full fledged type with limited legal values. That means the type has to be capitalized, and the values not. 416 | 417 | ```swift 418 | enum Suit { 419 | case hearts 420 | case diamonds 421 | case clubs 422 | case spades 423 | } 424 | // or 425 | enum Suit { 426 | case hearts, diamonds, clubs, spades 427 | } 428 | ``` 429 | 430 | Once defined, values can be defined of that type, and Swift's type inference capability can shorten the syntax somewhat. 431 | 432 | ```swift 433 | var card = Suit.clubs 434 | 435 | // since card is now bound to the type Suit, you can now do this: 436 | card = .spades 437 | ``` 438 | 439 | You can match on an enum value with `switch`, and it must either be exhaustive or have a default: 440 | 441 | ```swift 442 | switch card { 443 | case .spades: 444 | print("The swords of a soldier.") 445 | case .clubs: 446 | print("Weapons of war.") 447 | case .diamonds: 448 | print("Money for this art.") 449 | default: 450 | print("That's not the shape of my heart.") 451 | } 452 | ``` 453 | 454 | Enums are not natively iterable, but they can be converted into that easily: 455 | 456 | ```swift 457 | enum Suit: CaseIterable { 458 | case hearts, diamonds, clubs, spades 459 | } 460 | 461 | for s in Suit.allCases { 462 | print(s) 463 | } 464 | ``` 465 | 466 | Swift allows enums to have what it calls "associated values," creating what is variously called a "discriminated union" or "tagged union" depending on whom you ask. Each value can have its own set of associated values that could be the same or different. 467 | 468 | ```swift 469 | case Suit { 470 | case hearts(String) 471 | case diamonds(String) 472 | case clubs(String) 473 | case spades(String) 474 | } 475 | 476 | var threeOfDiamonds = Suit.diamond("3") 477 | ``` 478 | 479 | Each instance of an associated value enum is then not equal to another, even if they're of the same enum value. Seemingly the only way to get those values back out, though, is with pattern matching: 480 | 481 | ```swift 482 | switch card { 483 | case .spades(let value): 484 | print("The \(value) of Spades") 485 | case .clubs(let value): 486 | print("The \(value) of Clubs") 487 | case let .diamonds(value): 488 | print("The \(value) of Diamonds") 489 | case let .hearts(value): 490 | print("The \(value) of Hearts") 491 | } 492 | ``` 493 | 494 | For one-off cases, you can use `if let`. 495 | 496 | ```swift 497 | if case let .clubs(val) = card { 498 | print ("The \(val) of Clubs") 499 | } 500 | ``` 501 | 502 | (Those all do the same thing, but digging into the intricacies of Swift's pattern matching is out of scope for now.) 503 | 504 | Enums can *also* support "raw values," if specified explicitly, but they must be of the same primitive type: 505 | 506 | ```swift 507 | enum Suit: Character { 508 | case hearts = "H" 509 | case diamonds = "D" 510 | case clubs = "C" 511 | case spades = "S" 512 | } 513 | ``` 514 | 515 | If you list only one raw value, Swift will try to generate a raw value for the rest based on the type used. It's also possible to initialize an enum case from a raw value, if one was defined: 516 | 517 | ```swift 518 | let card = Suit(rawValue: "B") 519 | ``` 520 | 521 | This actually creates an "optional" of type `Suit?`, meaning it may or may not be legal and you have to explicitly check it. (Optionals are essentially a syntactic Maybe Monad, and way off topic.) 522 | 523 | You can even define an enumeration in terms of itself, which is just all kinds of weird. From the documentation: 524 | 525 | ```swift 526 | indirect enum ArithmeticExpression { 527 | case number(Int) 528 | case addition(ArithmeticExpression, ArithmeticExpression) 529 | case multiplication(ArithmeticExpression, ArithmeticExpression) 530 | } 531 | ``` 532 | 533 | And they go further by supporting methods on enumerations of all of the above types, the body of which would meaningfully have to be a switch: 534 | 535 | ```swift 536 | case Suit { 537 | case hearts(String) 538 | case diamonds(String) 539 | case clubs(String) 540 | case spades(String) 541 | 542 | func color: String { 543 | switch self { 544 | case .hearts: return "Red" 545 | case .diamonds: return "Red" 546 | case .clubs: return "Black" 547 | case .spades: return "Black" 548 | } 549 | } 550 | } 551 | 552 | print(Suit.clubs("3").color()); 553 | // Prints "Black" 554 | ``` 555 | 556 | Further reading: https://docs.swift.org/swift-book/LanguageGuide/Enumerations.html 557 | 558 | ### Rust 559 | 560 | As Rust's main syntactic goal seems to have been "Haskell, but with lots of curly braces," the language supports enumerations with and without associated values, either positional or named. 561 | 562 | All of the following are legal: 563 | 564 | ```rust 565 | // The values themselves. 566 | enum Suit { 567 | Hearts, 568 | Diamonds, 569 | Clubs, 570 | Spades, 571 | } 572 | 573 | // With one or more tuple associated values. 574 | enum Card { 575 | Hearts(i8), 576 | Diamonds(i8), 577 | Clubs(i8), 578 | Spades(i8), 579 | } 580 | 581 | // With one or more struct-associated values. 582 | enum Card { 583 | Hearts{val: i8}, 584 | Diamonds{val: i8}, 585 | Clubs{val: i8}, 586 | Spades{val: i8}, 587 | } 588 | 589 | // With an integer (only) explicit value. 590 | enum Suit { 591 | Hearts = 3, 592 | Diamonds = 4, 593 | Clubs = 5, 594 | Spades = 6, 595 | } 596 | ``` 597 | 598 | Enum values can be referenced scoped from their type, `Suit::Heart`, or first imported with `use Suit::*` and then used unqualified. Because they're a full type, they can be used in function signatures. 599 | 600 | Enums are almost always used with either `match` or `if let`, the latter of which being a sort of inverted way to care about only a single branch of a match. The `match` version must be exhaustive or have a default. 601 | 602 | ```rust 603 | let msg = match card { 604 | Suit::Spades => "Swords of a soldier".to_string(), 605 | Suit::Clubs => "Weapons of war".to_string(), 606 | Suit::Diamonds => "Money for this art".to_string(), 607 | _ => "Shape of my heart".to_string(), 608 | }; 609 | 610 | if let Diamonds(val) = card { 611 | println!("{} diamonds are a girl's best friend", val) 612 | } 613 | ``` 614 | 615 | The only way to extract associated values out of the enum is with pattern matching, which in Rust is almost absurdly robust: 616 | 617 | ```rust 618 | use Card::*; 619 | let the_val = match Card { 620 | Clubs(x) | Hearts(x) | Spades(x) | Diamonds(x) => x 621 | }; 622 | ``` 623 | 624 | As it's a full type, it can also have methods. Or in Rust-speak, "implementations," including of traits (what most languages would call an interface). They can do pretty much everything a `struct` can. Their body will in most cases be just a bit `match`. 625 | 626 | ```rust 627 | impl Suit { 628 | fn color(&self) -> String { 629 | match self { 630 | Self::Hearts => "Red".to_string(), 631 | Self::Diamonds => "Red".to_string(), 632 | Self::Clubs => "Black".to_string(), 633 | Self::Spades => "Black".to_string(), 634 | } 635 | } 636 | } 637 | ``` 638 | 639 | (The capitalized `Self` in this case is an implicit alias to `Suit`.) 640 | 641 | Further reading: https://doc.rust-lang.org/rust-by-example/custom_types/enum.html 642 | 643 | ### Kotlin 644 | 645 | Kotlin also has not one but two enum-esque systems: Enums and Sealed Classes. The difference between them is subtle and confusing. 646 | 647 | Enums are a class that inherits from an Enum class implicitly. 648 | 649 | ```kotlin 650 | enum class Suit { 651 | HEARTS, 652 | DIAMONDS, 653 | CLUBS, 654 | SPADES 655 | } 656 | ``` 657 | 658 | Each enum value is technically a "constant object." By default they're bare, but can also take int or string values constructor-style. 659 | 660 | ```kotlin 661 | enum class Suit(val abbrev: String) { 662 | HEARTS("H"), 663 | DIAMONDS("D"), 664 | CLUBS("C"), 665 | SPADES("S") 666 | } 667 | ``` 668 | 669 | Enums in Kotlin support methods, and unlike most languages here the methods may be defined separately for each value. Technically they're all implemented as subclasses, with the parent as an abstract base class. An enum can even support interfaces. 670 | 671 | ```kotlin 672 | interface Colorable { 673 | fun color() 674 | } 675 | 676 | enum class Suit(val abbrev: String) { 677 | HEARTS("H") { 678 | override fun color(): String = "Red" 679 | }, 680 | DIAMONDS("D") { 681 | override fun color(): String = "Red" 682 | }, 683 | CLUBS("C") { 684 | override fun color(): String = "Black" 685 | }, 686 | SPADES("S") { 687 | override fun color(): String = "Black" 688 | }; 689 | 690 | abstract fun color(): String 691 | 692 | fun abbreviation(): String { 693 | return this.abbrev 694 | } 695 | } 696 | ``` 697 | 698 | Enums have a number of built-in methods and properties, which make it possible to iterate an enum or get its value. That also makes them trivially serializable, unlike Sealed Classes. 699 | 700 | Sealed Classes, meanwhile, are almost like normal classes except that the list of subclasses is fixed at compile time and they must appear in the same source file. 701 | 702 | Whereas Enums are singletons, sealed classes may be singleton or instance-based. 703 | 704 | ```kotlin 705 | sealed class Action 706 | 707 | // This is a singleton sealed class 708 | object Quit: Action() 709 | 710 | // This is an instance-able sealed class 711 | class Move(val dir: String): Action() 712 | ``` 713 | 714 | Since they're objects/methods in their own right, they can have whatever methods you want, inherited or not. However, they do not have the automatic methods or properties of Enums that make them serializable. 715 | 716 | Kotlin supports a `when` syntax as an alternative to `switch` that is an expression, and can, in some cases, detect exhaustiveness. 717 | 718 | ```kotlin 719 | var result = when (card) { 720 | Suit.SPADES -> "The swords of a soldier" 721 | Suit.CLUBS -> "Weapons of war" 722 | Suit.DIAMONDS -> "Money for this art" 723 | else -> "The shape of my heart" 724 | } 725 | ``` 726 | 727 | Further reading: https://blog.kotlin-academy.com/enum-vs-sealed-class-which-one-to-choose-dc92ce7a4df5 728 | 729 | ### Scala 730 | 731 | Scala enums are also built on objects. 732 | 733 | ```scala 734 | package com.crell.poker { 735 | object Suit extends Enumeration { 736 | type Suit = Value 737 | val HEARTS, DIAMONDS, CLUBS, SPADES = Value 738 | } 739 | } 740 | 741 | // ... 742 | object Main extends App { 743 | import com.crell.poker.Suit._ 744 | 745 | var s = CLUBS 746 | 747 | // Iteration 748 | Suit.values foreach println 749 | } 750 | ``` 751 | 752 | They can carry values, including multiple values, which must be pre-set and not vary by instance. They also can support methods that way, although my Scala-fu is not strong enough to know if my syntax here is entirely correct. :-) 753 | 754 | ```scala 755 | object Suit extends Enumeration { 756 | protected case class Val(abbrev: String) extends super.Val { 757 | def color: String = abbrev.match { 758 | case Suit.HEARTS => "Red" 759 | case Suit.DIAMONDS => "Red" 760 | case Suit.CLUBS => "Black" 761 | case Suit.SPADES => "Black" 762 | } 763 | } 764 | type Suit = Value 765 | val HEARTS = Val("H") 766 | val DIAMONDS = Val("D") 767 | val CLUBS = Val("C") 768 | val SPADES = Val("S") 769 | } 770 | ``` 771 | 772 | Further reading: https://www.scala-lang.org/api/current/scala/Enumeration.html 773 | 774 | ## Summary 775 | 776 | Folded into a convenient table, a feature summary looks like this: 777 | 778 | | Language | C/C++ | Java | Python | Typescript | Haskell | F# (Union) | F# (Enum) | C# | Swift | Rust | Kotlin (Enums) | Kotlin (Sealed) | Scala 779 | |-------------------|--------|------|--------|------------|---------|------------|-----------|------|-------|------|----------------|-----------------|------- 780 | | Unit values | No | No | No | No | Yes | Yes | No | No | Yes | Yes | Yes | Ish? | Yes 781 | | Int values | Yes | Yes | Yes | Yes | No | No | Yes | Yes | Yes | Yes | Yes | Ish? | Yes 782 | | String values | No | Yes | Yes | Yes | No | No | No | No | Yes | No | Yes | Ish? | Yes 783 | | Associated values | No | No | No | No | Yes | No | No | No | Yes | Yes | No | Yes | No 784 | | Methods | No | Yes | Yes | No | No? | No | No | Ish | Yes | Yes | Yes | Yes | Yes 785 | | Type checked | Ish | Yes | No | Yes | Yes | No | Ish | Yes | Yes | Yes | Yes | Yes | Yes? 786 | | Iterable | No | Yes | Yes | No | No | No | No | Yes | Yes | No | Yes | No | No 787 | 788 | In terms of overall capability, Swift appears to have the edge with Rust a very close second. However, Rust also seems to have more powerful associated values ability (tuples or structs), and the usefulness of iterating enum types is debatable. I'm going to call it a qualified tie between those two in raw expressive power. 789 | 790 | ## Analysis 791 | 792 | Broadly speaking, I would separate the languages into a few categories: 793 | 794 | * **Fancy Constants**: C, Typescript, F# 795 | * **Fancy Objects**: Python, Java, C#, Scala 796 | * **Algebraic Data Types**: Haskell, Swift, Rust, Kotlin 797 | 798 | While they are superficially similar, and often use the same terminology, they approach the problem from different ways. The Fancy Constants languages are offering a syntactic convenience, but little else. Often they get compiled away at runtime, and their type checking may be incomplete. 799 | 800 | The Fancy Objects languages take that a step further and offer methods on enum types, which offers a centralized place to put a switch, match, or whatever branching syntax for RTTI. That is helpful, and helps with data modeling in ways that Fancy Constants do not. If the methods need to vary by enum type more than just a little, though, you run into some contortions and may find yourself better off with normal objects and interfaces. 801 | 802 | The main differentiator for ADT languages, as I'm using them here, is that they can be parameterized with different values. That offers another layer again of potential functionality and data modeling. It also becomes a natural and easy way to implement Monads in user space, and Haskell, Swift, and Rust all do exactly that in their core libraries, particularly for Maybe/Optional. That makes them an extremely robust way to handle data modeling in your application, and to "make invalid states unrepresentable," which is an excellent feature if you can get it. 803 | 804 | The downside is that once you start parameterizing enum values, you no longer get a guarantee that a Club is a Club is a Club. They may well be two different Clubs. The implementation details here around equality (a tricky subject in the best of circumstances) are the devil's hiding place. The other catch is that, as far as I can tell, no language with parameterized enum values lets you get at them easily without doing pattern matching. Depending on your use case that may be no big deal or may be a deal-breaker. In practice, I think it largely comes down to how easy the syntax is for pattern matching; Of all things I'd say Haskell is the nicest here, followed by Swift, then Rust. (Or possibly Rust then Swift, depending on your tastes. Rust gets very tricky when you have struct-parameterized enums.) 805 | 806 | 807 | https://twitter.com/Tojiro/status/823286025535393792 808 | -------------------------------------------------------------------------------- /c/.gitignore: -------------------------------------------------------------------------------- 1 | a.out -------------------------------------------------------------------------------- /c/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | typedef enum { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday } Day; 4 | 5 | 6 | void printer(Day d) { 7 | printf("The day is: %d\n", d); 8 | } 9 | 10 | 11 | int main() { 12 | 13 | Day d = Tuesday; 14 | 15 | printer(d); 16 | 17 | printer(4); 18 | 19 | return 0; 20 | } 21 | -------------------------------------------------------------------------------- /first-proposal.md: -------------------------------------------------------------------------------- 1 | 2 | ## For PHP 3 | 4 | As far as borrowing ideas for a PHP implementation, it seems silly at this point to not go all the way to ADT support if possible. "If possible" being the operative phrase, as PHP also lacks relevant features that some ADT languages use for their enums, such as pattern matching. In practice, I believe the only question is between Fancy Objects and ADTs. 5 | 6 | The main implementation questions would be: 7 | 8 | ### Backed by objects, or a new primitive? 9 | 10 | In practice, I see little reason to not build on objects. They're already there, and if we want methods and associated data and type checking then you're already 90% to objects. Most of the ADT implementations above build on objects, either implicitly or explicitly. 11 | 12 | ### Enums, Unions, or Sealed classes? 13 | 14 | The majority of ADT/fancy class languages here use a dedicated enum syntax of some variety. The exceptions are Kotlin's sealed classes and Haskell's union types, which are not strictly speaking enums but close enough. 15 | 16 | It has been suggested that the addition of a `typedef` to PHP to allow pre-definition of union types (already added in PHP 8.0, coming soon) would be "close enough" to enums to render any further effort unnecessary. That is true up to a point; the main limitation would be no way to enforce that all of the unioned types are type compatible: that they share an interface that makes it possible to type against them properly. It would also make the degenerate case of "I just want a closed list of options" more verbose. 17 | 18 | ```php 19 | typedef Suit = Hearts | Diamonds | Clubs | Spades; 20 | 21 | // This is just weird 22 | interface SuitInterface {} 23 | 24 | class Hearts implements SuitInterface {} 25 | class Diamonds implements SuitInterface {} 26 | class Clubs implements SuitInterface {} 27 | class Spades implements SuitInterface {} 28 | ``` 29 | 30 | Sealed classes would look essentially the same, give or take some syntax. Ideally we want an approach that "scales" cleanly from the basic case to the highly complex case. I think a properly designed dedicated Enum syntax is the best way to achieve that (even if it's just syntactic sugar that decomposes to the same as above). 31 | 32 | ### What types of values? 33 | 34 | There are largely 3 types of "single associated value" that enums can have: Unit (just the enum itself), Int only, or any primitive. The latter two get tricky if implemented as objects in PHP, unless it's just still-more sugar. I think Kotlin has the right idea here, though; Rather than giving an enum value its own direct primitive equivalent, make that a constant associated value. That is, no, you cannot define "Diamonds" to be equal to 3, but you can define "Diamonds" to be an object with a single property whose value is always 3. 35 | 36 | ### Equality 37 | 38 | This one gets really tricky, as equality is troublesome at the best of times. Already in PHP, when two objects are "equal" is somewhat confusing. 39 | 40 | ```php 41 | $a = new Foo(); 42 | $b = new Foo(); 43 | 44 | print $a == $a . PHP_EOL; // True 45 | print $a === $b . PHP_EOL; // False 46 | print $a == $b . PHP_EOL; // True 47 | ``` 48 | 49 | Depending on the implementation this may end up doing all kinds of weird thing that may or may not make sense. 50 | 51 | ### Proposal 52 | 53 | To simplify an implementation for PHP, I would recommend the following: 54 | 55 | * Enums are, mostly, sugar for abstract classes and inheritance. Yes, inheritance has its issues, but in this context its behavior is actually desireable. 56 | * Enum values may only be object instances. They may not map to primitives. However, a well-designed `__toString()` method can get us 80% of the way there with 10% of the effort, which seems like a good trade-off. Plus it emphasizes enum-as-unit, which I would argue is preferable from a modeling perspective in the majority case. 57 | * We allow some conceptual leakage in return for not needing pattern matching. This is another "80% of the benefit for 10% of the effort" case, and is consistent with Kotlin's approach anyway. 58 | 59 | For the simple case: 60 | 61 | ```php 62 | enum Suit { 63 | case Hearts; 64 | case Diamonds; 65 | case Clubs; 66 | case Spades; 67 | } 68 | ``` 69 | 70 | is equivalent to: 71 | 72 | ```php 73 | class Suit extends Enum implements IteratorAggregate { 74 | public static Hearts $Hearts; 75 | public static Diamonds $Diamonds; 76 | public static Clubs $Clubs; 77 | public static Spades $Spades; 78 | 79 | public function getIterator(): array { 80 | return [static::Hearts, static::Diamonds, static::Clubs, static::Spades]; 81 | } 82 | } 83 | 84 | class Hearts extends Suit {} 85 | class Diamonds extends Suit {} 86 | class Clubs extends Suit {} 87 | class Spades extends Suit {} 88 | 89 | Suit::$Hearts = new Hearts(); 90 | Suit::$Diamonds = new Diamonds(); 91 | Suit::$Clubs = new Clubs(); 92 | Suit::$Spades = new Spades(); 93 | ``` 94 | 95 | And assumes a base class like: 96 | 97 | ```php 98 | abstract class Enum { 99 | 100 | public static abstract function values(): array 101 | 102 | public function __toString(): string { 103 | // Via reflection: 104 | // If this object has no properties, return the name of the class. 105 | // If it has one property, return that property. 106 | // If it has multiple, concatenate them in lexical order and return that. 107 | // This method may of course be overridden. 108 | } 109 | } 110 | ``` 111 | 112 | The automatic `IteratorAggregate` implementation happens if and only if all of the defined `case`s is a unit. If any of them may have associated data, then it is not generated and the enum is not iterable. (This is consistent with Swift.) 113 | 114 | For enum members that can have one or more constant values applied to them: 115 | 116 | ```php 117 | enum Suit (public string $abbrev) { 118 | case Hearts("H"); 119 | case Diamonds("D"); 120 | case Clubs("C"); 121 | case Spades("S"); 122 | } 123 | ``` 124 | 125 | That is equivalent to: 126 | 127 | ```php 128 | class Suit extends Enum { 129 | public static Hearts $Hearts; 130 | public static Diamonds $Diamonds; 131 | public static Clubs $Clubs; 132 | public static Spades $Spades; 133 | 134 | public function __construct(public string $abbrev) {} 135 | 136 | public function getIterator(): array { 137 | return [static::Hearts, static::Diamonds, static::Clubs, static::Spades]; 138 | } 139 | } 140 | 141 | class Hearts extends Suit {} 142 | class Diamonds extends Suit {} 143 | class Clubs extends Suit {} 144 | class Spades extends Suit {} 145 | 146 | Suit::$Hearts = new Hearts("H"); 147 | Suit::$Diamonds = new Diamonds("D"); 148 | Suit::$Clubs = new Clubs("C"); 149 | Suit::$Spades = new Spades("S"); 150 | ``` 151 | 152 | Of note, if enum members are going to have constant values, they *must* all have the same constant values, and they *must* be defined in the enum itself using compact constructor syntax. Whether the value is public or private is then an explicit decision of the enum author. The enum author *may not*, however, directly implement a constructor. 153 | 154 | If *any one* of the enum members defines its own parameters, however, that means the following: 155 | 156 | 1. The enum itself may not define any constant values. 157 | 2. No iteration is defined. 158 | 3. It follows that any other enum members must be either units (no data at all) or have their own parameters. 159 | 160 | ```php 161 | enum Optional { 162 | case None; 163 | case Some(public mixed $value); 164 | } 165 | ``` 166 | 167 | Is equivalent to: 168 | 169 | ```php 170 | class Optional extends Enum { 171 | public static None $None; 172 | 173 | public static function Some(public mixed $value); 174 | } 175 | 176 | class None extends Optional {} 177 | 178 | class Some extends Optional { 179 | public function __construct(public mixed $value); 180 | } 181 | 182 | Optional::$None = new None(); 183 | ``` 184 | 185 | Again, it's the implementer's choice if the associated values are public or private. 186 | 187 | In all three cases, both the enum and its members may have methods, with the following restrictions: 188 | 189 | 1. Any methods defined on an individual member *must* be defined on the enum itself, either fully or abstract. 190 | 2. It therefore follows that all members *must* have an implementation of the same methods. 191 | 192 | This logic is lifted directly from Kotlin. 193 | 194 | 195 | ```php 196 | enum Optional { 197 | case None { 198 | public function valueOr(mixed $default): mixed { 199 | return $default; 200 | } 201 | }; 202 | 203 | case Some(protected mixed $value) { 204 | public function valueOr(mixed $default): mixed { 205 | return $this->value; 206 | } 207 | }; 208 | 209 | abstract public function valueOr(mixed $default): mixed; 210 | 211 | public function bind(callable $c): static { 212 | if ($this instanceof Optional::None) { 213 | return $this; 214 | } 215 | return static::Some($c($this->value)); 216 | } 217 | } 218 | ``` 219 | 220 | Is equivalent to: 221 | 222 | ```php 223 | class Optional extends Enum { 224 | public static None $None; 225 | 226 | public static function Some(protected mixed $value); 227 | 228 | public function bind(callable $c): static { 229 | if ($this instanceof Optional::None) { 230 | return $this; 231 | } 232 | return static::Some($c($this->value)); 233 | } 234 | } 235 | 236 | class None extends Optional { 237 | public function valueOr(mixed $default): mixed { 238 | return $default 239 | } 240 | } 241 | 242 | class Some extends Optional { 243 | public function __construct(public mixed $value); 244 | 245 | public function valueOr(mixed $default): mixed { 246 | return $this->value; 247 | } 248 | } 249 | 250 | Optional::$None = new None(); 251 | ``` 252 | 253 | As demonstrated above, whether a method is single-instanced and checks for the enum type internally or is split into separate methods is up to the implementer. 254 | 255 | The main enum may also implement interfaces if desired, in which case all the usual rules about inheritance and interfaces apply. 256 | 257 | ##### Implications 258 | 259 | * Most existing plumbing for classes and objects applies to enums if needed. They behave in a mostly predictable fashion. That includes being able to autoload the enum itself. If and when named parameters are adopted, creating a new associated-data enum member will automatically support that syntax. 260 | * While value-bound enums are not technically supported, the approach above combind with the `__toString()` method offer a "close enough" equivalent. 261 | * In the unit case, because member instances are singletons they will always `===` each other. In the associated data case, they will not. 262 | * Whether associated data should be public or private is punted to the implementer. Given PHP's general approach to visibility that seems the best option. On the plus side, it means any improvements made to PHP's visibility in the future should "just work" on enums as well. (Eg, if asymmetric visibility were ever added in a constructor-promotion-compatible way, they would become available to enums.) 263 | * Enum members do *not* support arbitrary member variables beyond those defined through constructor promotion. That is a deliberate limitation because if you need that much functionality, you don't want an enum. You want a traditional class. 264 | * Presumably Enums and Members could support attributes. What you would do with them I do not know, but I see no reason to not allow them. 265 | * The existence of a base `Enum` class gives us a place to put future extensions or functionality, such as a default `__serialize`, `__debugInfo`, or other such magic method implementations. 266 | 267 | #### Open questions 268 | 269 | * As shown here, enum members end up as public static class properties. That is suboptimal. Ideally they would be be write-once and exposed as though they were constants. This may be something that can be special cased in the engine. 270 | * It's unclear how to handle `instanceof` and references to the class. If implemented as pure sugar, that means members do become their own stand-alone objects and you'd do `$c instance of Diamonds`. That is suboptimal. It would be preferable to always make the name scoped to the enum itself, ie, `$c instanceof Suit::Diamonds`. However, it's unclear how that would interact with the `Diamonds()` member itself or with the static factory method in the case of associated data. This requires more investigation and possibly engine trickry. 271 | * If implemented as pure sugar, a side effect of this approach is that members become accessible as stand-alone classes to instantiate. That is not desireable. Ideally the actual implementation would be more robust and disallow that somehow. 272 | -------------------------------------------------------------------------------- /java/.gitignore: -------------------------------------------------------------------------------- 1 | *.class -------------------------------------------------------------------------------- /java/enum.java: -------------------------------------------------------------------------------- 1 | enum Card { 2 | HEARTS, 3 | DIAMONDS, 4 | CLUBS, 5 | SPADES; 6 | 7 | public String color() { 8 | switch (this) { 9 | case SPADES: 10 | return "Swords of a soldier"; 11 | case CLUBS: 12 | return "Weapons of war"; 13 | case DIAMONDS: 14 | return "Money for this art"; 15 | default: 16 | return "Shape of my heart"; 17 | } 18 | } 19 | } 20 | 21 | class Main { 22 | 23 | public static void main(String[] args) { 24 | for (Card c : Card.values()) { 25 | System.out.println(c); 26 | } 27 | 28 | System.out.println("Suit is: %s" + Card.valueOf("SPADES")); 29 | System.out.println("Suit is: %s" + Card.valueOf("SPADES").ordinal()); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /python/enumtest.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | class Suit(enum.Enum): 4 | HEARTS = enum.auto() 5 | DIAMONDS = enum.auto() 6 | CLUBS = 'C' 7 | SPADES = "S" 8 | 9 | def color(self): 10 | if self in [self.CLUBS, self.SPADES]: 11 | return "Black" 12 | else: 13 | return "Red" 14 | 15 | 16 | for card in (Suit): 17 | print ("As a string: " + str(card)) 18 | # print (card) 19 | print ("As a repr: ") 20 | print (repr(card)) 21 | print (card.color()) 22 | print('-----') 23 | 24 | print ("Are they equal?") 25 | print (Suit.HEARTS == Suit.HEARTS) 26 | -------------------------------------------------------------------------------- /rfc.md: -------------------------------------------------------------------------------- 1 | # PHP RFC: Enumerations and Algebraic Data Types 2 | 3 | * Date: 2020-09-19 4 | * Author: Larry Garfield (larry@garfieldtech.com), Ilija Tovilo (tovilo.ilija@gmail.com) 5 | * Status: Draft 6 | * Target Version: PHP 8.1 7 | * Implementation: TBD 8 | 9 | ## Introduction 10 | 11 | This RFC introduces Enumerations to PHP. Specifically, it introduces what are variously called "Algebraic Data Types", "tagged unions", or simply "enumerations" depending on the language. This capability offers greatly expanded support for data modeling, custom type definitions, and monad-style behavior. Enums enable the modeling technique of "make invalid states unrepresentable," which leads to more robust code with less need for exhaustive testing. 12 | 13 | Many languages have support for enumerations of some variety. A [survey we conducted of various languages](https://github.com/Crell/enum-comparison) found that they could be categorized into three general groups: Fancy Constants, Fancy Objects, and full Algebraic Data Types. For this implementation we opted to implement full Algebraic Data Types, as that offers the most robust set of functionality while also degrading gracefully to simpler use cases. (Or it progressively enhances to more complex use cases, depending on your point of view.) 14 | 15 | The specific implementation here draws inspiration primarily from Swift, Rust, and Kotlin, but is not (nor is it intended as) a perfect 1:1 port of any of them. Enumerations take many forms depending on the language, and we opted to implement the most robust combination of functionality feasible. Every piece of functionality described here exists in a similar form in at least one, usually several, other enumeration-supporting languages. It is implemented as a single RFC rather than a series of RFCs as the functionality all inter-relates, and if full ADTs are the goal (as we believe they should be) then it's easier to implement them at once rather than to dribble-in functionality in potentially disjoint pieces. 16 | 17 | The most popular case of enumerations is `boolean`, which is an enumerated type with legal values `true` and `false`. This RFC allows developers to define their own arbitrarily robust enumerations. 18 | 19 | ## Proposal 20 | 21 | ### Basic enumerations 22 | 23 | This RFC introduces a new language construct, `enum`. Enums are similar to classes, and share the same namespaces as classes, interfaces, and traits. They are also autoloadable the same way. An Enum defines a new type, which has a fixed, limited number of possible legal values. 24 | 25 | ```php 26 | enum Suit { 27 | case Hearts; 28 | case Diamonds; 29 | case Clubs; 30 | case Spades; 31 | } 32 | ``` 33 | 34 | This declaration creates a new enumerated type named `Suit`, which has four and only four legal values: `Suit::Hearts`, `Suit::Diamonds`, `Suit::Clubs`, and `Suit::Spades`. Variables may be assigned to one of those legal values. A function may be type checked against an enumerated type, in which case only values of that type may be passed. 35 | 36 | ```php 37 | $val = Suit::Diamonds; 38 | 39 | function pick_a_card(Suit $suit) { ... } 40 | 41 | pick_a_card($val); // OK 42 | pick_a_card(Suit::Clubs); // OK 43 | pick_a_card('Spades'); // throws TypeError 44 | ``` 45 | 46 | In the simple case, multiple cases may be defined on a single line. The following is semantically equivalent to the definition above. 47 | 48 | ```php 49 | enum Suit { 50 | case Hearts, Diamonds, Clubs, Spades; 51 | } 52 | ``` 53 | 54 | An Enumeration may have one or more `case` definitions, with no maximum, although at least one is required. 55 | 56 | Cases are not backed by a primitive value. That is, `Suit::Hearts` is not equal to 0. Instead, each case is backed by a singleton object of that name. That means that: 57 | 58 | ```php 59 | $a = Suit::Spades; 60 | $b = Suit::Spades; 61 | 62 | $a === $b; // true 63 | 64 | 65 | $a instanceof Suit; // true 66 | $a instanceof Suit::Spades; // true 67 | ``` 68 | 69 | ### Enumerated Case Methods 70 | 71 | As both Enum Types and Enum Cases are implemented using classes, they may take methods. The Enum Type may also implement an interface, which all Cases must then fulfill, directly or indirectly. 72 | 73 | ```php 74 | interface Colorful { 75 | public function color(): string; 76 | } 77 | 78 | enum Suit implements Colorful { 79 | case Hearts { 80 | public function color(): string { 81 | return "Red"; 82 | } 83 | }; // Note the semi-colon here! 84 | 85 | case Diamonds { 86 | public function color(): string { 87 | return "Red"; 88 | } 89 | }; 90 | 91 | case Clubs { 92 | public function color(): string { 93 | return "Black"; 94 | } 95 | }; 96 | 97 | case Spades { 98 | public function color(): string { 99 | return "Black"; 100 | } 101 | }; 102 | 103 | public function shape(): string { 104 | return "Rectangle"; 105 | } 106 | } 107 | 108 | function paint(Colorful $c) { ... } 109 | 110 | paint(Suit::Clubs); // Works 111 | ``` 112 | 113 | In this example, all four Enum cases will have a method `shape` inherited from `Suit`, and will all have their own method `color`, which they implement themselves. Case methods may be arbitrarily complex, and function the same as any other method. Additionally, magic methods such as `__toString` and friends may also be implemented and will behave like a normal method on an object. The one exception is `__construct`, which it not permitted. (See below.) 114 | 115 | Enum Cases may not implement interfaces themselves. 116 | 117 | Static methods on Cases are not supported. Static methods on the Enum Type are supported. 118 | 119 | (Ilija: We haven't discussed static methods at all. This is what makes the most sense to me at the moment but we can easily revisit this. I'm flexible.) 120 | 121 | Inside a method on a Case, The `$this` variable is defined and refers to the Case instance. (That is mainly useful with Associated Values. See below.) 122 | 123 | (Note that in this case it would be a better data modeling practice to also define a `SuitColor` Enum Type with values Red and Black and return that instead. However, that would complicate this example.) 124 | 125 | The above hierarchy is logically similar to the following class structure: 126 | 127 | ```php 128 | interface Colorful { 129 | public function color(): string; 130 | } 131 | 132 | abstract class Suit implements Colorful { 133 | public function shape(): string { 134 | return "Rectangle"; 135 | } 136 | } 137 | 138 | class Hearts extends Suit { 139 | public function color(): string { 140 | return "Red"; 141 | } 142 | } 143 | 144 | class Diamonds extends Suit { 145 | public function color(): string { 146 | return "Red"; 147 | } 148 | } 149 | 150 | class Clubs extends Suit { 151 | public function color(): string { 152 | return "Black"; 153 | } 154 | } 155 | 156 | class Spades extends Suit { 157 | public function color(): string { 158 | return "Black"; 159 | } 160 | } 161 | ``` 162 | 163 | ### Value listing 164 | 165 | The enumeration itself has an automatically generated static method `values()`. `values()` returns a packed array of all defined Cases in lexical order. 166 | 167 | ```php 168 | Suit::values(); 169 | // Produces: [Suit::Hearts, Suit::Diamonds, Suit::Clubs, Suit:Spades] 170 | ``` 171 | 172 | ### Primitive-Equivalent Cases 173 | 174 | By default, Enumerated Cases have no primitive equivalent. They are simply singleton objects. However, there are ample cases where an Enumerated Case needs to be able to round-trip to a database or similar datastore, so having a built-in primitive (and thus trivially serializable) equivalent defined intrinsically is useful. 175 | 176 | To define a primitive equivalent for an Enumeration, the syntax is as follows: 177 | 178 | ```php 179 | enum Suit: string { 180 | case Hearts = 'H'; 181 | case Diamonds = 'D'; 182 | case Clubs = 'C'; 183 | case Spades = 'S'; 184 | } 185 | ``` 186 | 187 | Primitive backing types of `int`, `string`, or `float` are supported, and a given enumeration supports only a single type at a time. (That is, no union of `int|string`.) If an enumeration is marked as having a primitive equivalent, then all cases must have a unique primitive equivalent defined. 188 | 189 | A Primitive-Equivalent Case will automatically down-cast to its primitive when used in a primitive context. For example, when used with `print`. 190 | 191 | ```php 192 | print Suit::Clubs; 193 | // prints "C" 194 | print "I hope I draw a " . Suit::Spades; 195 | // prints "I hope I draw a S". 196 | ``` 197 | 198 | Passing a Primitive Case to a primitive-typed parameter or return will produce the primitive value in weak-typing mode, and produce a `TypeError` in strict-typing mode. 199 | 200 | A Primitive-Backed enumeration also has a static method `from()` that is automatically generated. The `from()` method will up-cast from a primitive to its corresponding Enumerated Case. Invalid primitives with no matching Case will throw a `ValueError`. 201 | 202 | ```php 203 | $record = get_stuff_from_database($id); 204 | print $record['suit']; 205 | // Prints "H" 206 | $suit = Suit::from($record['suit']); 207 | $suit === Suit::Hearts; // True 208 | ``` 209 | 210 | A Primitive-Backed enumeration additionally has a method `list()` that returns an associated array of cases, in lexical order, keyed by their primitive equivalent. 211 | 212 | ```php 213 | $list = Suit::list(); 214 | $list === [ 215 | 'H' => Suit::Hearts, 216 | 'D' => Suit::Diamonds, 217 | 'C' => Suit::Clubs, 218 | 'S' => Suit::Spades, 219 | ]; // true 220 | ``` 221 | 222 | Primitive-backed Cases are not allowed to define a `__toString()` method, as that would create confusion with the primitive value itself. However, primitive-backed Cases are allowed to have other methods just like any other enum: 223 | 224 | ```php 225 | enum Suit: string { 226 | case Hearts = 'H'; 227 | case Diamonds = 'D'; 228 | case Clubs = 'C'; 229 | case Spades = 'S' { 230 | public function color(): string { return 'Black'; } 231 | } 232 | 233 | public function color(): string 234 | { 235 | // ... 236 | } 237 | } 238 | ``` 239 | 240 | ### Associated Values 241 | 242 | Enumerated Cases may optionally include associated values. An associated value is one that is associated with an instance of a Case. If a Case has associated values, it will **not** be implemented as a singleton. Each instance of the Case will then be its own object instance, so will not === another instance. 243 | 244 | Associated values are mutually exclusive with Primitive-Equivalent Cases. 245 | 246 | Associated values are defined using constructor property promotion. 247 | 248 | ```php 249 | enum Distance { 250 | case Kilometers(public int $num); 251 | case Miles(public int $num); 252 | } 253 | 254 | $my_walk = Distance::Miles(500); 255 | // Named parameters work like any other function call. 256 | $next_walk = Distance::Miles(num: 500); 257 | 258 | print $my_walk->num; // prints "500" 259 | 260 | $my_walk === $next_walk; // FALSE! 261 | ``` 262 | 263 | Enum Cases may not implement a full constructor. However, they may list parameters that will be auto-promoted to properties using constructor promotion. The visibility modifier is required. Cases may not implement properties other than promoted properties. 264 | 265 | An Enum Case that supports Associated Values is called an Associable Case. An Enum Case that does not have Associated Values is called a Unit Case. An Enumerated Type may consist of any combination of Associable and Unit Cases, but no Primitive-Equivalent Cases. 266 | 267 | The Enum Type itself may not define associated values. Only a Case may do so. 268 | 269 | Associated values are always read-only, both internally to the class and externally. Therefore, making them public does not pose a risk of 3rd party code modifying them inadvertently. They may, however, have attributes associated with them like any other property. 270 | 271 | On an Associable Case enumeration, the `values()` method is not available and will throw a `TypeError`. Since Associable Cases are technically unbounded, the method has no logical sense. 272 | 273 | Use cases that would require more complete class functionality (arbitrary properties, custom constructors, mutable properties, etc.) should be implemented using traditional classes instead. 274 | 275 | ### Match expressions 276 | 277 | When dealing with Unit Cases, `match` expressions offer a natural and convenient way to branch logic depending on the enum value. Since every instance of a Unit Case is a singleton, it will always pass an identity check. Therefore: 278 | 279 | ```php 280 | $val = Suit::Diamonds; 281 | 282 | $str = match ($val) { 283 | Suit::Spades => "The swords of a soldier", 284 | Suit::Clubs => "Weapons of war", 285 | Suit::Diamonds => "Money for this art", 286 | default => "The shape of my heart", 287 | } 288 | ``` 289 | 290 | That is not true when dealing with Associable Cases. Therefore, an alternate version of `match` is included. When `match` is suffixed with `type`, it will perform an `instanceof` check instead of an identity check. 291 | 292 | ```php 293 | $val = Distance::Miles(500); 294 | 295 | $str = match type ($val) { 296 | Distance::Kilometers => "Traveling $val->num km", 297 | Distance::Miles => "Traveling $val->num miles", 298 | } 299 | ``` 300 | 301 | (Ilija, your thoughts on this?) 302 | 303 | ### Examples 304 | 305 | Below are a few examples of Enums in action. 306 | 307 | #### Maybe 308 | 309 | The (in)famous Maybe Monad can be implemented like this: 310 | 311 | ```php 312 | enum Maybe { 313 | // This is a Unit Case. 314 | case None { 315 | public function bind(callable $f) { 316 | return $this; 317 | } 318 | }; 319 | 320 | // This is an Associable Case. 321 | case Some(private mixed $value) { 322 | // Note that the return type can be the Enum itself, thus restricting the return 323 | // value to one of the enumerated types. 324 | public function bind(callable $f) { 325 | // $f is supposed to return a Maybe itself. 326 | return $f($this->value); 327 | } 328 | }; 329 | 330 | // This method is available on both None and Some. 331 | public function value(): mixed { 332 | // Still need to sort out match() for this to make sense. 333 | return match type ($this) { 334 | Optional::None => throw new Exception(), 335 | Optional::Some => $this->val, 336 | }; 337 | } 338 | } 339 | ``` 340 | 341 | #### State machine 342 | 343 | Enums make it straightforward to express finite state machines. 344 | 345 | ```php 346 | enum OvenStatus { 347 | 348 | case Off { 349 | public function turnOn() { return OvenStatus::On; } 350 | }; 351 | 352 | case On { 353 | public function turnOff() { return OvenStatus::Off; } 354 | public function idle() { return OvenStatus::Idle; } 355 | }; 356 | 357 | case Idle { 358 | public function on() { return OvenStatus::On; } 359 | }; 360 | } 361 | ``` 362 | 363 | In this example, the oven can be in one of three states (Off, On, and Idling, meaning the flame is not on, but it will turn back on when it detects it needs to). However, it can never go from Off to Idle or Idle to Off; it must go through On state first. That means no tests need to be written or code paths defined for going from Off to Idle, because it's literally impossible to even describe that state. 364 | 365 | (Additional methods are of course likely in a real implementation.) 366 | 367 | #### Single Associable Enums 368 | 369 | Because all properties on an Enum are readonly, they offer a back-door way to create immutable objects. 370 | 371 | ```php 372 | enum Point { 373 | case ThreeD(public $x, public $x, public $z); 374 | } 375 | 376 | $p = Point::ThreeD(x: 3, y: 5, z: 7); 377 | 378 | print $p->y; // prints 5 379 | $p->z = 9; // throws an Error of some kind, TBD. 380 | ``` 381 | 382 | This is not a specific design goal of the implementation, but a potentially useful side effect. 383 | 384 | 385 | ## Backward Incompatible Changes 386 | 387 | "enum" and "type" become language keywords, with the usual potential for naming conflicts with existing global constants. 388 | 389 | ## Future Scope 390 | 391 | ### Pattern matching 392 | 393 | Most languages that have an equivalent of associated values also support pattern matching as a way to extract values from the Enum Case. Pattern matching allows for a single `match` branch to match on, for example, "any Foo::Bar instance where one of its two parameters is the number 5, and the other is extracted out into a variable to be used on the right." While a powerful feature in its own right, we believe that at this time it is not an MVP for useful Enumerations. It also has a large number of potential gotchas and complications all on its own, making it worthy of its own stand-alone RFC and development effort. 394 | 395 | For now, matching against the Enum Case and accessing properties directly (something not supported in most ADT-supporting languages) is "good enough" and has mostly self-evident semantics based on existing PHP patterns. 396 | 397 | ## Voting 398 | 399 | This is a simple yes/no vote to include Enumerations. 2/3 required to pass. 400 | 401 | ## References 402 | 403 | [Survey of enumerations supported by various languages, conducted by Larry](https://github.com/Crell/enum-comparison} 404 | -------------------------------------------------------------------------------- /rust/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /rust/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "enum-test" 5 | version = "0.1.0" 6 | 7 | -------------------------------------------------------------------------------- /rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "enum-test" 3 | version = "0.1.0" 4 | authors = ["Larry Garfield "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /rust/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::fmt::{self, Formatter, Display}; 4 | 5 | 6 | #[derive(Debug)] 7 | enum Suit { 8 | Hearts, 9 | Diamonds, 10 | Clubs, 11 | Spades, 12 | } 13 | 14 | #[derive(Debug)] 15 | enum Card { 16 | Hearts(i8), 17 | Diamonds(i8), 18 | Clubs(i8), 19 | Spades(i8), 20 | } 21 | 22 | impl Suit { 23 | fn color(&self) -> String { 24 | match self { 25 | Self::Hearts => "Red".to_string(), 26 | Self::Diamonds => "Red".to_string(), 27 | Self::Clubs => "Black".to_string(), 28 | Self::Spades => "Black".to_string(), 29 | } 30 | } 31 | } 32 | 33 | impl Card { 34 | fn pair_with(self, other: Self) -> bool { 35 | use Card::*; 36 | let the_val = match self { 37 | Clubs(x) | Hearts(x) | Spades(x) | Diamonds(x) => x 38 | }; 39 | 40 | let other_val = match other { 41 | Clubs(x) | Hearts(x) | Spades(x) | Diamonds(x) => x 42 | }; 43 | the_val == other_val 44 | } 45 | } 46 | 47 | impl Display for Card { 48 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 49 | match self { 50 | Self::Hearts(x) => write!(f, "{} of Hearts", x), 51 | Self::Diamonds(x) => write!(f, "{} of Diamonds", x), 52 | Self::Clubs(x) => write!(f, "{} of Clubs", x), 53 | Self::Spades(x) => write!(f, "{} of Spades", x), 54 | } 55 | } 56 | } 57 | 58 | impl Display for Suit { 59 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 60 | match self { 61 | Self::Hearts => write!(f, "Hearts"), 62 | Self::Diamonds => write!(f, "Diamonds"), 63 | Self::Clubs => write!(f, "Clubs"), 64 | Self::Spades => write!(f, "Spades"), 65 | } 66 | } 67 | } 68 | 69 | fn main() { 70 | println!("{}", Suit::Clubs); 71 | println!("{:?}", Suit::Hearts); 72 | println!("{}", Suit::Diamonds.color()); 73 | 74 | println!("{}", Card::Spades(4)); 75 | } 76 | --------------------------------------------------------------------------------