Solution (source code)

= Solution

The maximally variant safe declarations are:

``
trait Rel1[+X, +Y] { val pairs: List[(X, Y)] }
trait Rel2[-X, +Y] { def relatives(x: X): List[Y] }
trait Rel3[-X, -Y] { def related(x: X, y: Y): Boolean }
``

Leaving one or both parameters invariant is also legal but admits fewer <subtyping> conversions. These are <type variance> annotations.

For `Rel1`, both parameters occur only as outputs. Scala's immutable `List` and `Tuple2` are covariant, so a list of pairs of narrower types can safely be read as pairs of broader types. Thus both may be <covariant type parameters>. A contravariant declaration would permit, for example, treating a `Rel1[Any, Int]` containing `(7, 0)` as a `Rel1[String, Int]`, then reading its first component as a `String`. That would be unsound; the <compiler> rejects the negative annotation at this output position.

For `Rel2`, `X` occurs as a method argument and therefore may be a <contravariant type parameter>; `Y` is returned in an immutable list and may be a <covariant type parameter>. A function that accepts every `Any` is safe to use where only `String` queries are issued. The opposite direction would be unsafe: if `X` were covariant, the following hypothetical conversion could call a string-only implementation with an <integer>:

``
val strings: Rel2[String, Int] = new Rel2[String, Int] {
  def relatives(x: String): List[Int] = List(x.length)
}
// Unsafe if Rel2 were incorrectly covariant in X:
// val arbitrary: Rel2[Any, Int] = strings
// arbitrary.relatives(7)
``

Making `Y` contravariant would instead allow an integer-valued `Rel2[String, Any]` to be read as a string-valued `Rel2[String, String]`, recreating the output error.

For `Rel3`, both generic values are consumed and only a <Boolean function> result is produced, so both may be <contravariant type parameters>. A predicate accepting all values of broad types can answer queries restricted to narrower types. Covariance in either parameter would allow passing an unsupported value to a specialized implementation. For example, treating a string-only first-argument predicate as accepting `Any` would permit `related(7, y)` even though its body might call `x.length`. These are applications of the <variance-position rule>: method inputs reverse variance, whereas immutable outputs preserve it.<Scala 2.13 variance annotations specification>[Variance annotations and the change of position in method parameter types]{ref}