0

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)
  }
eancu
  • 53
  • 9
  • I tried to play around with what you gave me but it really seems like it's a guide to ensure stricter F-bounded types, and doesn't seem facilicitate removal of repetetive F-bounded type declarations on object methods – eancu Sep 29 '22 at 19:56
  • 1
    You can replace F-bound types with a type class. Your example looks like a Monoid[T] + some conversion T => HandScore – Mateusz Kubuszok Sep 30 '22 at 08:12
  • Okay, but then at least from what I can tell I would be replacing `[T <: HandPoints[T]]` in my method calls with `[T: HandPoints]` which shortens it but does not solve my issue – eancu Sep 30 '22 at 18:58

1 Answers1

1

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)
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • Ok, I think I see what you're saying. It's true that I don't consider `(score: T)` duplicated code but that's primarily because I don't think it can be avoided - however the type declaration can and you have indeed shown me how. Does the extracted code have to be a class? Meaning I have to initialise it to access the methods? I think it makes sense in this context but I wonder if there's a way to have it accessible globally – eancu Oct 01 '22 at 10:57
  • @eancu How can it be globally? What if in some class or method you don't want to have `T <: HandPoints[T]`? – Dmytro Mitin Oct 01 '22 at 17:10
  • @eancu You can generate bounds `T <: HandPoints[T]` with macro annotation but this would be confusing for everyone who will read your code. – Dmytro Mitin Oct 01 '22 at 17:11
  • 1
    I've marked this as accepted because it does alllow me to stop repeating the type declaration and I did suspect it was a code structure issue on my end. However, I have also noted that you can create a trait instead of a class, and create an object that implements a trait (which I did not expect I could do) so I could have those methods in the global namespace (even if I don't need to in this instance) – eancu Oct 01 '22 at 18:02