You need two concepts here. One is F-bounded polymorphism, the other is self-type constraints.
F-bounded polymorphism, without going into the underlying type theory, is essentially what makes binary operators work. You basically define a trait to have its parameter be a subtype of itself:
trait PartialOrdered[T <: PartialOrdered[T]] {
this: T =>
def below(that: T): Boolean
def <(that: T): Boolean =
(this below that) && !(that below this)
}
To make not just this below that
work, but also that below this
, we further need to constrain the self-type. This is done via this: T =>
, so that the compiler knows that this
is also an instance of T
, and not just PartialOrdered[T]
.
Then you define a class to use that trait. It needs to extend the trait with itself as the type parameter:
case class Pair(x: Double, y: Double) extends PartialOrdered[Pair] {
def below(that: Pair) =
x <= that.x && y <= that.y
}
object Program extends App {
println(Pair(1, 2) < Pair(2, 0))
println(Pair(1, 2) < Pair(1, 3))
println(Pair(1, 2) < Pair(0, 2))
println(Pair(1, 2) < Pair(2, 2))
}