Enumerations in EK9
An enumeration is a predefined list of values; the structure section outlines it. EK9 treats the list as ordered: a value's position is the order it is declared in. There is no enumeration or enum keyword; an enumeration is a simple type.
This page works through an example, including the operators every enumeration has as standard.
Enumeration Example
The example uses enumerations with records and functions. It shows every operator, how to 'stream' each value of an enumeration, and how to get an enumerated value from a String. If the String matches no value, the result is unset.
EK9's is set nature applies to enumerations too, unlike most other programming languages: an enumeration variable can hold none of the valid values, that is, be unset.
#!ek9 defines module site.enumerations.cards defines type CardRank Two Three Four Five Six Seven Eight Nine Ten Jack Queen King Ace CardSuit Hearts Diamonds Clubs Spades defines record Card rank as CardRank: CardRank() suit as CardSuit: CardSuit() Card() -> rank as CardRank suit as CardSuit this.rank: rank this.suit: suit override operator ? as pure <- rtn as Boolean: rank? and suit? operator $ as pure <- rtn as String: `${rank} of ${suit}` operator #^ as pure <- rtn as String: `${this}` defines function cardCreator() as abstract -> rank as CardRank <- rtn as Card? fullRankCreator() -> suit as CardSuit <- rtn as List of Card: cat CardRank | map with (suit) is cardCreator (rtn: Card(rank, suit)) | collect as List of Card defines program ShowOperators() stdout <- Stdout() hearts <- CardSuit.Hearts clubs <- CardSuit.Clubs require hearts <> clubs require hearts == #< clubs require CardSuit.Spades == #> clubs unknownSuit <- CardSuit() require ~unknownSuit? asString <- $hearts require asString == "Hearts" require hearts < clubs require clubs <=> hearts > 0 invalidSuit <- CardSuit("NonSuch") require ~invalidSuit? validSuit <- CardSuit("Clubs") require validSuit? validRank <- CardRank("Ace") require validRank? for rank in CardRank message <- switch rank <- rtn as String: String() case CardRank.Ace rtn: `Wow very lucky ${rank}` case > CardRank.Ten rtn: `Not too bad ${rank}` case > CardRank.Five rtn: `Oh dear me ${rank}` default rtn: `Not so lucky ${rank}` stdout.println(message) DeckOfPlayingCards() stdout <- Stdout() //List the four suits stdout.println("Cat Suits") cat CardSuit > stdout //List each of the Ranks stdout.println("Cat Ranks") cat CardRank > stdout stdout.println("With just a for loop") for suit in CardSuit stdout.println($suit) //If you wanted a list of suits suits <- cat CardSuit | collect as List of CardSuit stdout.println(`There are ${length suits} suits`) stdout.println("Procedural Deck creation") for suit in CardSuit for rank in CardRank stdout.println(Card(rank, suit)) //Now make a full deck of cards. stdout.println("Functional Deck creation") cat CardSuit | map with fullRankCreator | flatten > stdout
Download enumerations_cards.ek9
Running the program 'ShowOperators' prints:
Not so lucky Two Not so lucky Three Not so lucky Four Not so lucky Five Oh dear me Six Oh dear me Seven Oh dear me Eight Oh dear me Nine Oh dear me Ten Not too bad Jack Not too bad Queen Not too bad King Wow very lucky Ace
The Enumerations
The example is about playing cards, so it defines two enumerations: CardSuit and CardRank (Aces high). Each is a list of values under the name of the enumeration.
Using the Enumerations
A playing card is a record with both enumerations as properties. The record implements the operators is set, to string and promote. A class would also work; a record is enough here.
Functions
The abstract function cardCreator is extended by the dynamic function that fullRankCreator streams each rank through. If you have not read about functions and dynamic functions yet, read those sections first. The program DeckOfPlayingCards section below explains how they are used.
Operators
Program ShowOperators uses the operators on enumerations. They
are all generated from the value list; you write no default operator directives. The full
inventory is:
| Category | Operator(s) | Behaviour |
|---|---|---|
| Same-type comparison | == <> < > <= >= <=> |
Compare two enum values by declaration order (ordinal).
Hearts < Clubs is true because Hearts is declared before Clubs. |
| String-param comparison | == <> < > <= >= <=> with String |
Compare an enum value directly to a String.
hearts == "Hearts" works without any manual conversion. |
| String conversion | $ and #^ |
Returns the declared value name. $hearts gives "Hearts". |
| JSON serialization | $$ |
JSON of the value name. $$hearts gives the JSON string "Hearts". |
| Hash code | #? |
Ordinal-based hash code, suitable for use as Dict keys. |
| Is set | ? |
Tri-state check. CardSuit() creates an unset value;
CardSuit.Hearts is set. |
| First / Last | #< and #> |
On any value: #< clubs gives Hearts (first declared);
#> clubs gives Spades (last declared). The operators take a value,
not the type name (#< CardSuit is E07630). |
| Next / Previous | ++ and -- |
Move a variable to the next or previous declared value, in place: after suit++, a
suit that was Hearts is Diamonds. Moving past the last (or before
the first) value leaves it unset. |
| Constructors | CardSuit(), CardSuit(CardSuit), CardSuit(String) |
Default (creates unset value), copy, and construct-from-string. The string constructor returns an unset value if the string does not match any declared value. |
| Iteration | for suit in CardSuit |
Iterate all declared values in declaration order. |
That is 23 operators and 3 constructors, generated from a list of values. You write no comparison functions, string conversion methods, lookup utilities or serialisation code for an enumeration.
Enumerated values in a Switch
An enumeration in EK9 has no methods. Where Java or C# would put behaviour in an enum's methods, EK9 uses composition. In the example, a switch statement writes out an appraisal of the card drawn.
Keep the enumerated value apart from the business logic (here, the appraisal). When the two need a closer binding:
- use a class with a property of the enumeration and a method holding the appraisal logic;
- or use one or more functions for the appraisal;
- or combine a class with a function delegate.
To attach other information to an enumeration (an image for each playing card, say), use a Dictionary with the enumerated value as the key and the image as the value.
Why EK9 does it this way. Methods on enumerations are not possible in EK9, and EK9's view is that they are not desirable. Separating the values from the logic is composition, which in general is more flexible in the longer term. You might argue it is less Object-Oriented, and it is; but EK9 lets you choose between classes and plain data structures with functions that operate on them.
Read some of Alan Perlis's quotes, in particular "It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures". Object-Orientation is not the only way to structure a solution.
Program DeckOfPlayingCards
This program shows how to reach every enumerated value:
- The first two parts stream each value of CardSuit and CardRank through a pipeline, convert it to a String and print it to 'stdout'.
- The third converts an enumeration to a 'List of ...', should you need one.
- A double nested loop creates each Card and prints it to stdout, using the enumeration type as if it were a collection of values (which in a way it is).
- The last line does the same as the double nested loop, functionally: CardSuit is streamed into a map function, fullRankCreator.
fullRankCreator accepts each CardSuit sent into the pipeline. It creates a dynamic function (a bit like a lambda) that captures the suit and extends the abstract function cardCreator, then streams each CardRank into it.
The dynamic function uses the captured CardSuit and the streamed CardRank to create a Card record. The new Cards are collected into rtn, a List of Card.
So a List of Card comes out of the pipeline for each CardSuit. To print every Card to 'stdout', flatten takes each card out of its List of Card. The promote operator written on the Card record turns each Card into a String, so it can be printed.
Summary
Enumerations are simple, so this page shows how to use them. It contrasts a functional approach (stream pipelines, functions, and dynamic functions that capture variables) with the double nested loop over CardSuit and CardRank.
- When the processing is as simple as this, a procedural approach is probably the better option.
- When you find complex logic deep in nested loops, or want to break out of them (EK9 has no
break), take a functional approach.
EK9 lets you choose functional, Object-Oriented, or a blend of the two.
Next Steps
The next section, Records, introduces the first aggregate type.