Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala: How to make case class copy keep manifest information

A case classes copy() method is supposed to make an identical copy of the instance, plus replacing any fields by name. This seems to fail when the case class has type parameters with manifests. The copy loses all knowledge of the types of its parameters.

case class Foo[+A : Manifest](a: A) {
  // Capture manifest so we can observe it
  // A demonstration with collect would work equally well
  def myManifest = implicitly[Manifest[_ <: A]]
}

case class Bar[A <: Foo[Any]](foo: A) {
  // A simple copy of foo
  def fooCopy = foo.copy()
}

val foo = Foo(1)
val bar = Bar(foo)

println(bar.foo.myManifest)     // Prints "Int"
println(bar.fooCopy.myManifest) // Prints "Any"

Why does Foo.copy lose the manifest on the parameters and how do I make it retain it?

like image 805
drhagen Avatar asked Aug 11 '12 22:08

drhagen


2 Answers

Several Scala peculiarities interact to give this behavior. The first thing is that Manifests are not only appended to the secret implicit parameter list on the constructor but also on the copy method. It is well known that

case class Foo[+A : Manifest](a: A)

is just syntactic sugar for

case class Foo[+A](a: A)(implicit m: Manifest[A])

but this also affects the copy constructor, which would look like this

def copy[B](a: B = a)(implicit m: Manifest[B]) = Foo[B](a)(m)

All those implicit ms are created by the compiler and sent to the method through the implicit parameter list.

This would be fine as long as one was using the copy method in a place where the compiler knew Foos type parameter. For example, this will work outside of the Bar class:

val foo = Foo(1)
val aCopy = foo.copy()
println(aCopy.myManifest) // Prints "Int"

This works because the compiler infers that foo is a Foo[Int] so it knows that foo.a is an Int so it can call copy like this:

val aCopy = foo.copy()(manifest[Int]())

(Note that manifest[T]() is a function that creates a manifest representation of the type T, e.g. Manifest[T] with a capital "M". Not shown is the addition of the default parameter into copy.) It also works within the Foo class because it already has the manifest that was passed in when the class was created. It would look something like this:

case class Foo[+A : Manifest](a: A) {
  def myManifest = implicitly[Manifest[_ <: A]]

  def localCopy = copy()
}

val foo = Foo(1)
println(foo.localCopy.myManifest) // Prints "Int"

In the original example, however, it fails in the Bar class because of the second peculiarity: while the type parameters of Bar are known within the Bar class, the type parameters of the type parameters are not. It knows that A in Bar is a Foo or a SubFoo or SubSubFoo, but not if it is a Foo[Int] or a Foo[String]. This is, of course, the well-known type erasure problem in Scala, but it appears as a problem here even when it doesn't seem like the class is doing anything with the type of foos type parameter. But it is, remember there is a secret injection of a manifest every time copy is called, and those manifests overwrite the ones that were there before. Since the Bar class has no idea was the type parameter of foo is, it just creates a manifest of Any and sends that along like this:

def fooCopy = foo.copy()(manifest[Any])

If one has control over the Foo class (e.g. it's not List) then one workaround it by doing all the copying over in the Foo class by adding a method that will do the proper copying, like localCopy above, and return the result:

case class Bar[A <: Foo[Any]](foo: A) {
  //def fooCopy = foo.copy()
  def fooCopy = foo.localCopy
}

val bar = Bar(Foo(1))
println(bar.fooCopy.myManifest) // Prints "Int"

Another solution is to add Foos type parameter as a manifested type parameter of Bar:

case class Bar[A <: Foo[B], B : Manifest](foo: A) {
  def fooCopy = foo.copy()
}

But this scales poorly if class hierarchy is large, (i.e. more members have type parameters, and those classes also have type parameters) since every class would have to have the type parameters of every class below it. It also seems to make the type inference freak out when trying to construct a Bar:

val bar = Bar(Foo(1)) // Does not compile

val bar = Bar[Foo[Int], Int](Foo(1)) // Compiles
like image 59
drhagen Avatar answered Nov 13 '22 01:11

drhagen


There are two problems as you identified. The first problem is the type erasure problem inside of Bar, where Bar doesn't know the type of Foo's manifest. I would personally use the localCopy workaround you suggested.

The second issue is that another implicit is being secretly injected into copy. That issue is resolved by explicitly passing the value again into the copy. For example:

scala> case class Foo[+A](a: A)(implicit val m: Manifest[A @uncheckedVariance])
defined class Foo

scala> case class Bar[A <: Foo[Any]](foo: A) {
     | def fooCopy = foo.copy()(foo.m)
     | }
defined class Bar

scala> val foo = Foo(1)
foo: Foo[Int] = Foo(1)

scala> val bar = Bar(foo)
bar: Bar[Foo[Int]] = Bar(Foo(1))

scala> bar.fooCopy.m
res2: Manifest[Any] = Int

We see the copy has kept the Int manifest but the type of fooCopy and res2 is Manifest[Any] due to erasure.

Because I needed access to the implicit evidence to do the copy I had to use the explicit implicit (hah) syntax instead of the context bound syntax. But using the explicit syntax caused errors:

scala> case class Foo[+A](a: A)(implicit val m: Manifest[A])
<console>:7: error: covariant type A occurs in invariant position in type => Manifest[A] of value m
       case class Foo[+A](a: A)(implicit val m: Manifest[A])
                                         ^
scala> case class Foo[+A](a: A)(implicit val m: Manifest[_ <: A])
defined class Foo

scala> val foo = Foo(1)
<console>:9: error: No Manifest available for Int.

WTF? How come the context bound syntax works and the explicit implicit doesn't? I dug around and found a solution to the problem: the @uncheckedVariance annotation.

UPDATE

I dug around some more and found that in Scala 2.10 case classes have been changed to only copy the fields from the first parameter list in copy().

Martin says: case class ness is only bestowed on the first argument list the rest should not be copied.

See the details of this change at https://issues.scala-lang.org/browse/SI-5009.

like image 1
sourcedelica Avatar answered Nov 13 '22 01:11

sourcedelica