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
}