= Solution
Declare `+U`, keep its stored <linked list> immutable, and generalize operations that need to accept output values by a fresh <lower type bound>:
``
class PartialFn[T, +U](val data: List[(T, U)]) {
def get(x: T): Option[U] =
data.find { case (key, _) => key == x }.map(_._2)
def remove(x: T): PartialFn[T, U] =
new PartialFn[T, U](data.filterNot { case (key, _) => key == x })
def add[V >: U](x: T, y: V): PartialFn[T, V] =
new PartialFn[T, V]((x, y) ::
data.filterNot { case (key, _) => key == x })
def compose[V >: U, W](that: PartialFn[V, W]): PartialFn[T, W] = {
val pairs: List[(T, W)] = data.flatMap {
case (x, y) => that.get(y).map(z => (x, z)).toList
}
new PartialFn[T, W](pairs)
}
}
``
`get`, `remove`, and the immutable `val data` expose `U` in covariant result positions: <Scala>'s immutable `List`, `Tuple2` and `Option` preserve that position. The old `add(x:T,y:U)` consumed `U` directly, violating the <variance-position rule>. The new `add` accepts a value of a chosen supertype `V` and returns a new mapping with that widened output type; it never inserts the wider value into the old narrower mapping.
The original `compose` also mentions `U` as the first, invariant parameter of `PartialFn[U,V]`. Changing only `add` is therefore insufficient. Its new first <type parameter> `V >: U` is independent of the class's covariant parameter except for the allowed lower bound. Any stored `y:U` is acceptable to `that.get` as a `V`, while the result has output type `W`. This <lower type bound> solution avoids unchecked variance annotations or casts. The official variance rules illustrate the same lower-bound widening technique for an immutable collection's consuming method; the later 2.13 text corroborates the rule, without identifying an installed 2018 compiler version.<Scala 2.13 variance annotations specification>[Variance Annotations: method-parameter positions, invariant type arguments, and the lower-bound widening example for Sequence.append]{ref}
Back to article page