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:
Bar
covariant in A
, then the problem hides itself because now Bar[_$1] <: Bar[Any]
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.
Scala case classes are just regular classes which are immutable by default and decomposable through pattern matching. It uses equal method to compare instance structurally. It does not use new keyword to instantiate object. All the parameters listed in the case class are public and immutable by default.
A Scala Case Class is like a regular class, except it is good for modeling immutable data. It also serves useful in pattern matching, such a class has a default apply() method which handles object construction. A scala case class also has all vals, which means they are immutable.
The one of the topmost benefit of Case Class is that Scala Compiler affix a method with the name of the class having identical number of parameters as defined in the class definition, because of that you can create objects of the Case Class even in the absence of the keyword new.
The Scala interface for Spark SQL supports automatically converting an RDD containing case classes to a DataFrame. The case class defines the schema of the table. The names of the arguments to the case class are read using reflection and they become the names of the columns.
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[_]
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With