AnyVal — bridge generated for member method clashes with definition of the member itself

I have the following value class definition:

final class Box[A](val value: A) extends AnyVal

If I try to create a trait like this, it works just fine:

import cats.Functor

implicit def functor: Functor[Box] =
  new Functor[Box] {
    def map[A, B](fa: Box[A])(f: A => B): Box[B] =
      new Box(f(fa.value))
  }

Unfortunately the following fails with a nasty compiler error:

// Fails!
final class BoxFunctor extends Functor[Box] {
  def map[A, B](fa: Box[A])(f: A => B): Box[B] =
    new Box(f(fa.value))
}

And this too fails with a compiler error:

implicit def functor: Functor[Box] =
  // Fails here!
  new BoxFunctor {}

//  This seems fine, but we can't instantiate it in any way :-(
trait BoxFunctor extends Functor[Box] {
  def map[A, B](fa: Box[A])(f: A => B): Box[B] =
    new Box(f(fa.value))
}

The error looks like this:

[error] Box.scala:33:11: bridge generated for member method map: [A, B](fa: .Box[A], f: A => B).Box[B] in trait BoxFunctor
[error] which overrides method map: [A, B](fa: F[A], f: A => B)F[B] in trait Functor
[error] clashes with definition of the member itself;
[error] both have erased type (fa: Object, f: Function1)Object
[error]       new BoxFunctor {}
[error]           ^

So I don’t understand it …

Why does it work with anonymous classes (e.g. new Functor[Box] {}) and why does it fail as soon as I give that class implementation a name?

Any way to workaround it?

1 Like

Hey, theres an issue on dotty repo for this, but apparently Travis found a workaround

@bishabosha I don’t think that issue is related. There’s multiple ways that value class erasure can lead to member clashes, the issue is about dotty erasing differently than scalac and causing clashes with existing code, whereas this thread is about scalac seemlingly erasing similar code (anonymous vs non-anonymous classes) differently.

3 Likes

Looks like https://github.com/scala/bug/issues/10082, which was declared “wontfix”:

@retronym said:
This is a known limitation of the encoding of value classes. Unfortunately the best we can do is issue the error here.

More for my own understanding than anything else. If we take this minimization

class Box(val value: Any) extends AnyVal
abstract class Foo[A] { def foo(a: A): A }
class Bar extends Foo[Box] { def foo(a: Box) = a }

then the compiler errors because it tries to generate bytecode equivalent to the following (pseudo) code:

class Box(val value: Object)

abstract class Foo { 
  def foo(a: Object): Object 
}

class Bar extends Foo { 
  /* This is the clashing method.
   * It contains the actual implementation.
   * It operates on unboxed values.
   */
  def foo(a: Object): Object = a

  /* This method is supposed to override `foo` in `Foo`. 
   * It calls the "real" method and is responsible for boxing and unboxing.
   */
  override def foo(a: Object): Object = {
    new Box(foo(a.asInstanceOf[Box].value))
  }
}

The strange thing is, in the case of an anonymous class scalac name mangles the “real” method.

scala> :pa -raw
// Entering paste mode (ctrl-D to finish)

class Box(val value: Any) extends AnyVal
abstract class Foo[A] { def foo(a: A): A }
object A { val bar = new Foo[Box] { def foo(a: Box) = a } }

// Exiting paste mode, now interpreting.

scala> :javap -p A$$anon$1
Compiled from "<pastie>"
public final class A$$anon$1 extends Foo<Box> {
  public java.lang.Object A$$anon$$foo(java.lang.Object);
  public java.lang.Object foo(java.lang.Object);
  public A$$anon$1();
}

I first thought this was some side effect of the compiler thinking that that method is “effectively private”. But apparently he only mangles the name when the erased signatures are the same. So it seems to be intentional.

scala> :pa -raw
// Entering paste mode (ctrl-D to finish)

class Box(val value: Int) extends AnyVal
abstract class Foo[A] { def foo(a: A): A }
object A { val bar = new Foo[Box] { def foo(a: Box) = a } }

// Exiting paste mode, now interpreting.

scala> :javap -p A$$anon$1
Compiled from "<pastie>"
public final class A$$anon$1 extends Foo<Box> {
  public int foo(int);
  public java.lang.Object foo(java.lang.Object);
  public A$$anon$1();

Wouldn’t it be possible with a little extra effort to do that name mangling for publicly accessible methods as well? Or is it only possible to keep track of that mangled name in the local scope/compilation unit?