scalagenericstype-boundsf-bounded-polymorphism

How can I avoid duplicating a type bound in an object class in Scala?


Yes I have checked the very similarly titled question but the answer given is not as helpful to me as I am new to Scala and am having trouble understanding it.

I'm writing some functions that check a list of cards and return a score based on the result of the list. Technically, it checks a list of groups of cards, however I am simplifying the code for the purposes of this question.

Now, I want these functions to be extensible to different types of scoring. For instance, if all the cards are Hearts, then we may give them 1 point. However, in another ruleset, it may give 3 points.

I have a points wrapper that translates to a final score. This means that a type of point may translate to a different final score than another type of point. The purpose here is to allow you to customise the scoring and play the card game in a slightly different way.

You will see in the sample code below, but I end up getting a lot of repetition in my method declarations, namely having to write [T <: HandPoints[T]] over and over again.

All of the def methods have been written in an object, so I cannot add the type parameter to the class.

I imagine there's probably a neat solution to extract these methods outside of the class, but I want the methods that check the cards to not be repeated, so it makes a lot of sense to me to have them declared statically in an object

Here is the HandPoints trait:

trait HandPoints[T] {
  def toHandScore: HandScore
  def zero: T
  def add(that: T): T
}

case class RegularPoint(points: Int) extends HandPoints[RegularPoint] {
  override def toHandScore: HandScore = HandScore(points)
  override def zero: RegularPoint = RegularPoint(0)
  override def add(that: RegularPoint): RegularPoint = RegularPoint(points + that.points)
}

case class DoublingPoints(points: Int) extends HandPoints[DoublingPoints] {
  override def toHandScore: HandScore = HandScore(points*2)
  override def zero: DoublingPoints = DoublingPoints(0)
  override def add(that: DoublingPoints): DoublingPoints = DoublingPoints(points + that.points)
}

case class HandScore(score: Int) {

}

Here are the functions I wrote to assess the cards

  
  trait Card {
    def getValue: Int
    def getSuit: String
  }


  def scored[T <: HandPoints[T]](score: T)(boolean: Boolean): T = {
    if (boolean) score else score.zero
  }

  def isAllEvens[T <: HandPoints[T]](score: T)(cards: List[Card]): T = {
    scored(score) {
      cards.forall(_.getValue % 2 == 0)
    }
  }

  def isAllReds[T <: HandPoints[T]](score: T)(cards: List[Card]): T = {
    scored(score) {
      cards.forall(List("HEARTS", "DIAMONDS").contains(_))
    }
  }

  def isAllNoDuplicates[T <: HandPoints[T]](score: T)(cards: List[Card]): T = {
    scored(score) {
      cards.distinct == cards
    }
  }
  
  val regularGameCriteria: List[List[Card] => RegularPoint] = List(
    isAllEvens(RegularPoint(1)),
    isAllReds(RegularPoint(3)),
    isAllNoDuplicates(RegularPoint(5))
  )
  
  val beginnerGameCriteria: List[List[Card] => RegularPoint] = List(
    isAllEvens(RegularPoint(1)),
    isAllReds(RegularPoint(1)),
    isAllNoDuplicates(RegularPoint(1))
  )

  val superGameCriteria: List[List[Card] => DoublingPoints] = List(
    isAllEvens(DoublingPoints(1)),
    isAllReds(DoublingPoints(3)),
    isAllNoDuplicates(DoublingPoints(5))
  )
  
  def countScore[T <: HandPoints[T]](scoreList: List[List[Card] => T])(melds: List[Card]): T = {
    scoreList.map(f => f(melds)).reduce((a, b) => a.add(b))
  }

  def regularGameScore(cards: List[Card]): RegularPoint = {
    countScore(regularGameCriteria)(cards)
  }

  def beginnerGameScore(cards: List[Card]): RegularPoint = {
    countScore(beginnerGameCriteria)(cards)
  }

  def superGameScore(cards: List[Card]): DoublingPoints = {
    countScore(superGameCriteria)(cards)
  }

Solution

  • Firstly, you can look how F-bounded polymorphism can be replaced with ad hoc polymorphism (type classes):

    https://tpolecat.github.io/2015/04/29/f-bounds.html

    Secondly, the bounds [T <: HandPoints[T]] in different methods are actually not code duplication. Different T in different methods are different type parameters. You just called them with the same letter. Bounds for one type parameter do not restrict another type parameter.

    I'm curious why you consider a code duplication the [T <: HandPoints[T]] in different methods and not (score: T) or (cards: List[Card]). I guess because most people think about terms, considering types less important.

    Thirdly, you should start to explore OOP (or FP with type classes or some their mix) i.e. organize your methods into classes/objects (or type classes) with some behavior. Now a bunch of (static) methods looks like procedural programming.

    For example, for a start we can introduce two classes HandPointsHandler and Criteria (you can pick up better names):

    case class HandScore(score: Int)
    
    trait HandPoints[T] {
      def toHandScore: HandScore
      def zero: T
      def add(that: T): T
    }
    
    case class RegularPoint(points: Int) extends HandPoints[RegularPoint] {
      override def toHandScore: HandScore = HandScore(points)
      override def zero: RegularPoint = RegularPoint(0)
      override def add(that: RegularPoint): RegularPoint = RegularPoint(points + that.points)
    }
    
    case class DoublingPoints(points: Int) extends HandPoints[DoublingPoints] {
      override def toHandScore: HandScore = HandScore(points*2)
      override def zero: DoublingPoints = DoublingPoints(0)
      override def add(that: DoublingPoints): DoublingPoints = DoublingPoints(points + that.points)
    }
    
    trait Card {
      def getValue: Int
      def getSuit: String
    }
    
    // new class
    class HandPointsHandler[T <: HandPoints[T]] {
      def scored(score: T)(boolean: Boolean): T =
        if (boolean) score else score.zero
    
      def isAllEvens(score: T)(cards: List[Card]): T =
        scored(score) {
          cards.forall(_.getValue % 2 == 0)
        }
    
      def isAllReds(score: T)(cards: List[Card]): T =
        scored(score) {
          cards.forall(List("HEARTS", "DIAMONDS").contains)
        }
    
      def isAllNoDuplicates(score: T)(cards: List[Card]): T =
        scored(score) {
          cards.distinct == cards
        }
    
      def countScore(scoreList: List[List[Card] => T])(melds: List[Card]): T =
        scoreList.map(_.apply(melds)).reduce(_ add _)
    }
    
    // new class
    class Criteria[T <: HandPoints[T]](handler: HandPointsHandler[T], points: List[T]) {
      val gameCriteria: List[List[Card] => T] = {
        List(
          handler.isAllEvens _,
          handler.isAllReds _,
          handler.isAllNoDuplicates _
        ).zip(points).map { case (f, point) => f(point) }
      }
    }
    
    val points135 = List(1, 3, 5)
    val points111 = List(1, 1, 1)
    
    val regularPointsHandler = new HandPointsHandler[RegularPoint]
    
    val regularGameCriteria: List[List[Card] => RegularPoint] =
      new Criteria[RegularPoint](regularPointsHandler, points135.map(RegularPoint)).gameCriteria
    
    val beginnerGameCriteria: List[List[Card] => RegularPoint] =
      new Criteria[RegularPoint](regularPointsHandler, points111.map(RegularPoint)).gameCriteria
    
    val doublingPointsHandler = new HandPointsHandler[DoublingPoints]
    
    val superGameCriteria: List[List[Card] => DoublingPoints] =
      new Criteria[DoublingPoints](doublingPointsHandler, points135.map(DoublingPoints)).gameCriteria
    
    def regularGameScore(cards: List[Card]): RegularPoint =
      regularPointsHandler.countScore(regularGameCriteria)(cards)
    
    def beginnerGameScore(cards: List[Card]): RegularPoint =
      regularPointsHandler.countScore(beginnerGameCriteria)(cards)
    
    def superGameScore(cards: List[Card]): DoublingPoints =
      doublingPointsHandler.countScore(superGameCriteria)(cards)