How to restrict method parameter to subclass type in Scala

会有一股神秘感。 提交于 2021-02-08 10:12:49

问题


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): G

Error:(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]


回答1:


  1. You probably want <: (subtype) instead of : (context bound).

  2. 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").

  3. 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)
}



回答2:


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(_ |+| _)


来源:https://stackoverflow.com/questions/59800677/how-to-restrict-method-parameter-to-subclass-type-in-scala

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!