Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 280
Nikita Avatar asked Jul 13 '20 10:07

Nikita


People also ask

What does case class do in scala?

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.

Are Case classes immutable Scala?

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.

Why use case classes in scala?

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.

What is the use of case class in spark?

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.


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[_].

like image 119
Alexey Romanov Avatar answered Sep 29 '22 17:09

Alexey Romanov