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

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:

CategoryOperator(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:

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:

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.

EK9 lets you choose functional, Object-Oriented, or a blend of the two.

Next Steps

The next section, Records, introduces the first aggregate type.