Why is my Card equal to another Card of the same rank but a different suit, and how do I order cards another way?

← Operators and Expressions · Ref: Q2280

Because the sameness your Card uses is hierarchy-wide: a '<=>' written by hand on a trait (or an abstract base) over what every card provides. Here the trait Ranked orders by 'rank()' alone, so the suit is not part of it and Card(5, hearts) == Card(5, spades). That is deliberate - the author of the trait said once what makes two cards the same - and a type that implements the trait cannot change it (E07182), and no E07238 is raised for the fields it adds.

To tell suits apart, put the suit into the trait's ordering: write the trait's '<=>' over rank() and then suit() (RankedAndSuited above), and SuitedCard(5, hearts) == SuitedCard(5, spades) is false.

Other views of the same cards - by value alone, or 'aces high' - are not a second '<=>'. A type has ONE sameness; every other order is a comparator function given to 'sort': 'sort by acesHigh'. Ace low is the natural order of the trait; the comparator ranks the ace as 14.

If each card type should be compared only with its own kind instead, leave the trait without a '<=>' and give each class 'default operator' (per-type sameness): it compares every field.

Example

#!ek9
defines module qa.operators.card.suit.sameness

  defines constant
    hearts <- "hearts"
    spades <- "spades"

  defines trait

    <?-
      Hierarchy-wide sameness, written by hand over what every card provides: the rank, and only the rank.
    -?>
    Ranked
      rank() as pure abstract
        <- rtn as Integer?

      operator <=> as pure
        -> other as Ranked
        <- rtn as Integer: rank() <=> other.rank()

      operator #? as pure
        <- rtn as Integer: #?rank()

    <?-
      The same idea with the suit in the ordering: rank first, then suit.
    -?>
    RankedAndSuited
      rank() as pure abstract
        <- rtn as Integer?

      suit() as pure abstract
        <- rtn as String?

      operator <=> as pure
        -> other as RankedAndSuited
        <- rtn as Integer: rank() <=> other.rank()
        if rtn == 0
          rtn: suit() <=> other.suit()

      operator #? as pure
        <- rtn as Integer: #?rank() + #?suit()

  defines class

    Card with trait of Ranked
      cardRank as Integer: 0
      cardSuit as String: String()

      Card()
        ->
          rank as Integer
          suit as String
        this.cardRank: rank
        this.cardSuit: suit

      override rank() as pure
        <- rtn as Integer: cardRank

      suit() as pure
        <- rtn as String: cardSuit

      override operator ? as pure
        <- rtn as Boolean: cardRank? and cardSuit?

      operator $ as pure
        <- rtn as String: `${cardRank} of ${cardSuit}`

    SuitedCard with trait of RankedAndSuited
      cardRank as Integer: 0
      cardSuit as String: String()

      SuitedCard()
        ->
          rank as Integer
          suit as String
        this.cardRank: rank
        this.cardSuit: suit

      override rank() as pure
        <- rtn as Integer: cardRank

      override suit() as pure
        <- rtn as String: cardSuit

      override operator ? as pure
        <- rtn as Boolean: cardRank? and cardSuit?

      operator $ as pure
        <- rtn as String: `${cardRank} of ${cardSuit}`

  defines function

    <?-
      Another view of the same cards is a comparator function given to sort, never a second '<=>'.
    -?>
    acesHigh() as pure
      ->
        first as Card
        second as Card
      <- rtn as Integer: high(first) <=> high(second)

    high() as pure
      -> card as Card
      <- rtn as Integer: card.rank() == 1 <- 14 else card.rank()

    flag()
      -> answer as Boolean
      <- rtn as String: answer? <- $answer else "unset"

  defines program

    CardSuitSameness()
      stdout <- Stdout()

      fiveOfHearts <- Card(5, hearts)
      fiveOfSpades <- Card(5, spades)
      stdout.println(`Card 5 hearts == Card 5 spades: ${flag(fiveOfHearts == fiveOfSpades)}`)

      suitedHearts <- SuitedCard(5, hearts)
      suitedSpades <- SuitedCard(5, spades)
      stdout.println(`SuitedCard 5 hearts == SuitedCard 5 spades: ${flag(suitedHearts == suitedSpades)}`)

      ace <- Card(1, "clubs")
      king <- Card(13, hearts)
      two <- Card(2, spades)
      hand <- cat [ace, king, two] | collect as List of Card

      byRank <- cat hand | sort | collect as List of Card
      stdout.println(`by rank: ${byRank}`)

      aceHigh <- cat hand | sort by acesHigh | collect as List of Card
      stdout.println(`aces high: ${aceHigh}`)

Common mistakes

E07182 — Card implements Ranked, whose '<=>' is written by hand over rank(): that is hierarchy-wide sameness, sealed for every type that implements the trait. Card cannot declare or default its own. To include the suit, change the trait's '<=>'; for another order, use a comparator function with 'sort by'. See ek9 -h E07182.

Incorrect:

      suit() as pure
        <- rtn as String: cardSuit

      default operator <=>

Correct:

      suit() as pure
        <- rtn as String: cardSuit
Other ways to ask this
  • Why is Card(5, hearts) == Card(5, spades) true when the suit differs?
  • Why does a field a subclass adds not take part in the sameness of a trait?
  • How do I compare cards by value, and differently with aces high?
  • How do I get more than one ordering of the same type?

Coming from another language?

Java: a Comparable written on an interface or base class and used by equals can ignore the fields a subclass adds, and several orderings are separate Comparator objects given to sort. Kotlin: sortedWith(compareBy ...) for other views. EK9: the trait's hand-written '<=>' is the single sameness; other views are comparator functions given to 'sort by'.

Keywords: <=>, ==, E07182, E07238, aces high, card, comparator, hierarchy-wide, rank, sameness, sort by, suit, trait