5

I have defined two typeclasses:

trait WeakOrder[-X] { self =>
  def cmp(x: X, y: X): Int
  def max[Y <: X](x: Y, y: Y): Y = if (cmp(x, y) >= 0) x else y
  def min[Y <: X](x: Y, y: Y): Y = if (cmp(x, y) <= 0) x else y
}

trait Lattice[X] { self =>
  def sup(x: X, y: X): X
  def inf(x: X, y: X): X
}

I would like to do the following:

trait TotalOrder[-X] extends Lattice[X] with WeakOrder[X] { self =>
  def sup(x: X, y: X): X = max(x, y)
  def inf(x: X, y: X): X = min(x, y)
}

But this is impossible because contravariant type X appears at a covariant position (the returning value of sup and inf).

However, semantically this is correct: max and min with the type signature max[Y <: X](x: Y, y: Y): Y encodes the fact that the returning value of max / min must be one of the two arguments.

I tried to do the following:

trait TotalOrder[-X] extends Lattice[X] with WeakOrder[X] { self =>
  def sup[Y <: X](x: Y, y: Y): Y = max(x, y)
  def inf[Y <: X](x: Y, y: Y): Y = min(x, y)
}

However, the method def sup[Y <: X](x: Y, y: Y): Y cannot inherit def sup[X](x: X, y: X): X. The compiler complains that the type signature does not match. But the former one (with the on-site variance annotation) imposes a stronger type restrictions than the latter signature. Why the former one cannot inherit the latter one? How can I bypass the contravariant type restrictions on TotalOrder[-X] (semantically, a total order is contravariant)?

Tongfei Chen
  • 613
  • 4
  • 14

1 Answers1

0

This is not semantically correct. It should be clear from the definition of covariant and contravariant, but I'll try to give an example:

Suppose we have hierarchy of entities:

class Shape(s:Float)
class Circle(r:Float) extends Shape(Math.PI.toFloat * r * r)

And let's assume that it's possible to create contravariant orders, as you tried:

trait CircleOrder extends TotalOrder[Circle] {
   // compare by r
}

trait ShapeOrder extends TotalOrder[Shape] {
  // compare by s
}

By definition of contravariants, as Shape <: Circle, CircleOrder <: ShapeOrder (CircleOrder is supertype of ShapeOrder)

Suppose we have client that takes CircleOrder as the argument and uses it to compare circles:

def clientMethod(circleOrder:TotalOrder[Circle]) = {
  val maxCircle = circleOrder.max(???, ???) // expected to return Circle
  maxCircle.r // accessing field that is present only in circle
}

Then, by definition of inheritance, it should be possible to pass ShapeOrder instead of CircleOrder (remember, ShapeOrder is subtype):

clientMethod(new ShapeOrder {/*...*/})

Obviously it will not work, as client still expects order to return Circles, not Shapes.

I think in your case the most reasonable approach will use regular generics.

Update

This is how you can ensure type safety, but it's a bit ugly.

    trait WeakOrder[-X] {
      def cmp(x: X, y: X): Int
      def max[T](x: X with T, y: X with T): T =
        if (cmp(x, y) >= 0) x else y
      def min[T](x: X with T, y: X with T): T =
        if (cmp(x, y) <= 0) x else y
    }

    trait Lattice[X] {
      def sup[T](x: X with T, y: X with T): T
      def inf[T](x: X with T, y: X with T): T
    }

    trait TotalOrder[-X] extends Lattice[X] with WeakOrder[X] {
      def sup[T](x: X with T, y: X with T): T = max(x, y)
      def inf[T](x: X with T, y: X with T): T = min(x, y)
    }
Aivean
  • 10,692
  • 25
  • 39
  • I understand what you are saying, and that's what I tried to do by the signature `max[Y <: X](x: Y, y: Y): Y`. passing two circles to a `ShapeOrder`'s `max` function should return a `Circle` but not a `Shape` because Circle <: Shape; thus the result type of `max` should be `Circle`. – Tongfei Chen Jul 28 '15 at 01:49
  • @TongfeiChen Making type a contravariant implies that you need to have possibility to do something like this: `val cOrd:CircleOrderByR = new ShapeOrderByS`. Are you sure that this is what you need? I would argue that different orders should be unrelated. – Aivean Jul 28 '15 at 02:25
  • A total order, by its mathematical definition, is an order which is antisymmetric, transitive and total. By totality we mean that every two elements can be compared. A total order relies on only one function `cmp` (with signature (X, X) => Int) to define, thus it is naturally contravariant. E.g., A total order on real numbers (Double) naturally implies a total order on integers (Int). – Tongfei Chen Jul 28 '15 at 03:38
  • @TongfeiChen So you are saying, that having implementation of `TotalOrder[Circle]` we can use is to find max of two `Shapes`? – Aivean Jul 28 '15 at 06:01
  • `Circle <: Shape`; thus `TotalOrder[Shape] <: TotalOrder[Circle]` (hence contravariance). This means that an order on shapes is an order on circles; i.e. an implementation of a TotalOrder[Shape] can be used to find the max of two Circles. – Tongfei Chen Jul 28 '15 at 22:37
  • @TongfeiChen ok, I finally got your point. Please check the update to my answer. – Aivean Jul 30 '15 at 06:59
  • Yeah this works. Thank you :-) But i found out an easier workaround: @scala.annotation.unchecked.uncheckVariance ... – Tongfei Chen Aug 05 '15 at 14:44