Reputation: 343
I have a trait GameStatistics
that defines an add()
method that takes a parameter and returns the sum of itself and the parameter. Implementations in subclasses should only accept instances of their own type as a parameter (or maybe also subtypes).
I would like to use this add
method to aggregate lists of GameStatistics
, using Seq's reduce
method.
I have not been able to define this in Scala and make it compile. Below is one example that I tried plus its compile errors.
The errors don't make any sense to me. How should I get this to work?
package bgengine
trait GameStatistics {
def equity: Double
def add[G: this.type](s: G): G
def multiply(x: Double): GameStatistics
}
object GameStatistics {
def aggregate(stats: Seq[GameStatistics]): GameStatistics = stats.reduce( _ add _ )
}
case class SimpleGameStatistics(equity: Double, nrGames: Int) extends GameStatistics {
override def add[G: SimpleGameStatistics](s: G): G =
SimpleGameStatistics((equity * nrGames + s.equity * s.nrGames) / (nrGames + s.nrGames), nrGames + s.nrGames).asInstanceOf[G]
override def multiply(x: Double): SimpleGameStatistics = SimpleGameStatistics(equity * x, nrGames)
}
Error:(6, 12) GameStatistics.this.type does not take type parameters
def add[G: this.type](s: G): GError:(17, 21) bgengine.SimpleGameStatistics does not take type parameters override def add[G: SimpleGameStatistics](s: G): G =
Error:(18, 48) value equity is not a member of type parameter G SimpleGameStatistics((equity * nrGames + s.equity * s.nrGames) / (nrGames + s.nrGames), nrGames + s.nrGames).asInstanceOf[G]
Error:(18, 59) value nrGames is not a member of type parameter G SimpleGameStatistics((equity * nrGames + s.equity * s.nrGames) / (nrGames + s.nrGames), nrGames + s.nrGames).asInstanceOf[G]
Error:(18, 83) value nrGames is not a member of type parameter G SimpleGameStatistics((equity * nrGames + s.equity * s.nrGames) / (nrGames + s.nrGames), nrGames + s.nrGames).asInstanceOf[G]
Error:(18, 105) value nrGames is not a member of type parameter G SimpleGameStatistics((equity * nrGames + s.equity * s.nrGames) / (nrGames + s.nrGames), nrGames + s.nrGames).asInstanceOf[G]
Upvotes: 1
Views: 1965
Reputation: 48410
Consider typeclass approach
case class SimpleGameStatistics(equity: Double, nrGames: Int)
trait GameStatistics[G] {
def add(a: G, b: G): G
def multiply(x: Double, a: G): G
}
object GameStatistics {
implicit val simpleGameStatistics = new GameStatistics[SimpleGameStatistics] {
def add(a: SimpleGameStatistics, b: SimpleGameStatistics) = SimpleGameStatistics((a.equity * a.nrGames + b.equity + b.nrGames) / (a.nrGames + b.nrGames), a.nrGames + b.nrGames)
def multiply(x: Double, a: SimpleGameStatistics) = SimpleGameStatistics(a.equity * x, a.nrGames)
}
implicit class StatsOps[G](private val a: G) {
def add(b: G)(implicit ev: GameStatistics[G]): G = ev.add(a, b)
def multiply(x: Double)(implicit ev: GameStatistics[G]): G = ev.multiply(x, a)
}
implicit class AggregateOps[G](private val stats: List[G]) {
def aggregateStats(implicit ev: GameStatistics[G]): G = stats.reduce(_ add _)
}
}
import GameStatistics._
SimpleGameStatistics(42, 7) add SimpleGameStatistics(8, 43)
List(SimpleGameStatistics(42, 7), SimpleGameStatistics(8, 43)).aggregateStats
SimpleGameStatistics(42, 7) multiply 7
which outputs
import GameStatistics._
res0: SimpleGameStatistics = SimpleGameStatistics(6.9,50)
res1: SimpleGameStatistics = SimpleGameStatistics(6.9,50)
res2: SimpleGameStatistics = SimpleGameStatistics(294.0,7)
Note these sort of "addition" binary operations are a very common pattern for which cats provides an abstraction called Semigroup
, hence if we provide Semigroup
instance for SimpleGameStatistics
import cats.Semigroup
implicit val intAdditionSemigroup: Semigroup[SimpleGameStatistics] =
(a: SimpleGameStatistics, b: SimpleGameStatistics) => SimpleGameStatistics((a.equity * a.nrGames + b.equity + b.nrGames) / (a.nrGames + b.nrGames), a.nrGames + b.nrGames)
we can hook into all the goodies cats
provides out-of-the-box such as |+|
infix operator
import cats.implicits._
SimpleGameStatistics(42, 7) |+| SimpleGameStatistics(8, 43)
List(SimpleGameStatistics(42, 7), SimpleGameStatistics(8, 43)).reduce(_ |+| _)
Upvotes: 5
Reputation: 170713
You probably want <:
(subtype) instead of :
(context bound).
this.type
doesn't mean what you think it means (it is the type which only has this
(and null
) as value, not "the current type").
If you fixed those problems, the cast in
override def add[G <: SimpleGameStatistics](s: G): G =
SimpleGameStatistics((equity * nrGames + s.equity * s.nrGames) / (nrGames + s.nrGames), nrGames + s.nrGames).asInstanceOf[G]
wouldn't make sense; you just created an instance of SimpleGameStatistics
, casting it to a subclass would throw an exception.
But it looks like you want F-bounded polymorphism:
trait GameStatistics[G <: GameStatistics[G]] { this: G =>
def equity: Double
def add(s: G): G
def multiply(x: Double): G
}
object GameStatistics {
def aggregate[G <: GameStatistics[G]](stats: Seq[G]): G = stats.reduce( _ add _ )
}
case class SimpleGameStatistics(equity: Double, nrGames: Int) extends GameStatistics[SimpleGameStatistics] {
override def add(s: SimpleGameStatistics): SimpleGameStatistics =
SimpleGameStatistics((equity * nrGames + s.equity * s.nrGames) / (nrGames + s.nrGames), nrGames + s.nrGames)
override def multiply(x: Double): SimpleGameStatistics = SimpleGameStatistics(equity * x, nrGames)
}
Upvotes: 3