When defining a trait, it should be possible to specify functions, which the companion object of an implementing class must define.
Syntax might be:
trait TMyTrait {
// members which the extending class must define
} withCompanion {
// members which the companion object of the extending class must define
}
Example:
trait TCollection[N, TSelf] {
def elements: Iterable[N]
def add(el: N): TSelf
}
/**
* store elements in different collections, according to their categories
* @tparam N type of elements to store
* @tparam TColl type of underlying collections
* @tparam TCont type of this container of collections
*/
trait TContainer[N, TColl <: TCollection[N, TColl], TCont <: TContainer[N, TColl, TCont]] {
def category(el: N): Int
def collections: Map[Int, TColl]
def add(el: N): TCont
}
final case class DoubleCollection(override val elements: Set[Double]) extends TCollection[Double, DoubleCollection] {
override def add(el: Double): DoubleCollection = DoubleCollection(elements + el)
}
object DoubleCollection {
val empty = DoubleCollection(Set.empty[Double])
}
final case class DoubleContainer(override val collections: Map[Int, DoubleCollection])
extends TContainer[Double, DoubleCollection, DoubleContainer] {
override def category(el: Double): Int = Math.log(el).floor.toInt
override def add(el: Double): DoubleContainer = {
val cat = category(el)
val newCollection = collections.getOrElse(cat, DoubleCollection.empty).add(el)
DoubleContainer(collections.updated(cat, newCollection))
}
}
Note that the function add
can’t be defined in trait TContainer
, because it uses DoubleCollection.empty
. So when defining the same structure for e.g. Float, Decimal or Rational, we need to re-implement this function add
, only because we can’t access the underlying function empty
in the trait’s code.
With the proposed feature however, the function add
could be defined on trait level:
class sip2 {
trait TCollection[N, TSelf] {
def elements: Iterable[N]
def add(el: N): TSelf
} withCompanion {
def empty: TSelf
}
/**
* store elements in different collections, according to their categories
*/
trait TContainer[N, TColl <: TCollection[N, TColl], TCont <: TContainer[N, TColl, TCont]] {
def category(el: N): Int
def collections: Map[Int, TColl]
override def add(el: N): TCont = {
val cat = category(el)
// in the following, `TColl.empty` translates to the companion object's function
// `empty`, which is known to the compiler due to the `withCompanion`-block
val newCollection = collections.getOrElse(cat, TColl.empty).add(el)
DoubleContainer(collections.updated(cat, newCollection))
}
}
final case class DoubleCollection(override val elements: Set[Double]) extends TCollection[Double, DoubleCollection] {
override def add(el: Double): DoubleCollection = DoubleCollection(elements + el)
}
object DoubleCollection {
// the following definition is enforced by the compiler
val empty = DoubleCollection(Set.empty[Double])
}
final case class DoubleContainer(override val collections: Map[Int, DoubleCollection])
extends TContainer[Double, DoubleCollection, DoubleContainer] {
override def category(el: Double): Int = Math.log(el).floor.toInt
}
The withCompanion
block should also allow defining members on companion objects of extending classes:
trait TBase {
// ...
} withCompanion {
def toBeDefinedOnCompanionObject: Any
}
trait TMiddle extends TBase withCompanion {
override def toBeDefinedOnCompanionObject: Any = new Object()
}
class MyClass extends TMiddle {
// ...
}
object MyClass {
// implementation of `toBeDefinedOnCompanionObject` is already defined by TMiddle
}