Scala case class copy doesn't always work with `_` existential type

Viewed 195

I'm trying to copy() a Scala case class which has a type param. At the call site, the type of the value is Foo[_].

This compiles as expected:

case class Foo[A](id: String, name: String, v1: Bar[A])
case class Bar[A](v: A)

val foo: Foo[_] = Foo[Int]("foo1", "Foo 1", Bar[Int](1))

foo.copy(id = "foo1.1")

But if I add another member of type Bar[A], it doesn't compile anymore:

case class Foo[A](id: String, name: String, v1: Bar[A], v2: Bar[A])
case class Bar[A](v: A)

val foo: Foo[_] = Foo[Int]("foo1", "Foo 1", Bar[Int](1), Bar[Int](2))

foo.copy(id = "foo1.1") // compile error, see below
type mismatch;
 found   : Playground.Bar[_$1]
 required: Playground.Bar[Any]
Note: _$1 <: Any, but class Bar is invariant in type A.
You may wish to define A as +A instead. (SLS 4.5)
Error occurred in an application involving default arguments

Scastie

So far I found two workarounds:

  • Make Bar covariant in A, then the problem hides itself because now Bar[_$1] <: Bar[Any]
  • Define a copyId(newId: String) = copy(id = newId) method on Foo and call that instead, then we aren't calling copy on a value of type Foo[_].

However, neither of those are really feasible for my use case, Bar should be invariant, and I have too many different copy calls on Foo[_] instances to make copyThisAndThat methods for them all.

I guess my real question is, why is Scala behaving this way? Seems like a bug tbh.

1 Answers

After the compiler handles named and default parameters, the calls become

foo.copy("foo1.1", foo.name, foo.v1)

and

foo.copy("foo1.1", foo.name, foo.v1, foo.v2)

respectively. Or, if you replace the parameters with types,

foo.copy[?](String, String, Bar[_])

and

foo.copy[?](String, String, Bar[_], Bar[_])

? is the type parameter of copy which has to inferred. In the first case the compiler basically says "? is the type parameter of Bar[_], even if I don't know what that is".

In the second case the type parameters of two Bar[_] must really be the same, but that information is lost by the time the compiler is inferring ?; they are just Bar[_], and not something like Bar[foo's unknown type parameter]. So e.g. "? is the type parameter of first Bar[_], even if I don't know what that is" won't work because so far as the compiler knows, the second Bar[_] could be different.

It isn't a bug in the sense that it follows the language specification; and changing the specification to allow this would take significant effort and make both it and the compiler more complicated. It may not be a good trade-off for such a relatively rare case.

Another workaround is to use type variable pattern to temporarily give a name to _:

foo match { case foo: Foo[a] => foo.copy(id = "foo1.1") }

The compiler now sees that foo.v1 and foo.v2 are both Bar[a] and so the result of copy is Foo[a]. After leaving the case branch it becomes Foo[_].

Related