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)
}
CodePudding user response:
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)