I think leaving it that having any java interface as a parent kills your shot at being a value class will be a mistake. The essence of being a "value class" is statelessness. Java interfaces are stateless by definition. They ought to be perfect candidates. And they are.
If we can't require people to "opt in" to an AnyRef parent, we can still allow people to opt out.
// the interface of java.lang.CharSequence, translated exactly
// to scala so it can extend Any.
trait CharSequence extends Any {
def length: Int
def charAt(index: Int): Char
def subSequence(start: Int, end: Int): CharSequence
}
// I opt out of an AnyRef parent for CharSequence like this.
final class Bippy(val xs: Array[Char]) extends AnyVal with CharSequence {
def length = xs.length
def charAt(index: Int): Char = xs(index)
def subSequence(start: Int, end: Int): CharSequence = new Bippy(xs.slice(start, end))
}
If a Bippy has to be passed to something looking for a CharSequence, then we box it, exactly like we already do in that situation.
object Test {
// public CharSequence f(char[]);
def f(x: Bippy) = x.subSequence(0, 1)
// public CharSequence g(CharSequence);
def g(x: CharSequence) = x.subSequence(0, 1)
}
What concerns me beyond the obvious is the damage it is likely to do to java interop to have a cliff like this. If a single java interface poisons your value class, then in nontrivial situations you will certainly be forced to choose between performance and interop. It's a bitter pill under any circumstances, but it would be especially bitter here because, again, interfaces are stateless by definition.
Imagine a Bippy value class wrapping an Int which implements Comparable[Bippy] (or some identical interface with a different name, since Comparable is still special-cased.) Here is a method.
def f(x1: Bippy, x2: Bippy) = x1 compareTo x2
This is what the implementation looks like if Comparable <: Any.
public int (int, int);
0: getstatic #19 // Field jl/Bippy$.MODULE$:Ljl/Bippy$;
3: iload_1
4: iload_2
5: invokevirtual #22 // Method jl/Bippy$.compareTo$extension:(II)I
8: ireturn
This is what it looks like if Comparable <: AnyRef.
public int f(rl.Bippy, rl.Bippy);
0: aload_1
1: aload_2
2: invokevirtual #20 // Method jl/Bippy.compareTo:(Ljl/Bippy;)I
5: ireturn
It must be somewhere between 2 and 10 times slower - mighty rigorous handwaving I realize. I'll measure it if the exact size is at issue, but I suppose if 2x isn't enough, then 10x won't be either.