32
votes

Scala 2.10 introduced value classes. They are very usefull for writing typesafe code. Moreover there are some limitations, some of them will be detected by compiler, and some will require allocation in runtime.

I want to create value classes using case class syntax, to allow creating-without-new-syntax and human-friendly toString. No pattern matching, cause it requires allocation.

So the question is: will using case class syntax require value class allocation?

4

4 Answers

38
votes

You can have a case class that is a value class. As you can see from the example below, there is no object creation. Except of course the inevitable boxing if you would upcast to Any.

Here is a little piece of scala code

class ValueClass(val value:Int) extends AnyVal

case class ValueCaseClass(value:Int) extends AnyVal

class ValueClassTest {

  var x: ValueClass = new ValueClass(1)

  var y: ValueCaseClass = ValueCaseClass(2)

  def m1(x:ValueClass) = x.value

  def m2(x:ValueCaseClass) = x.value
}

And the bytecode, which does not contain the slightest trace of the two value classes.

Compiled from "ValueClassTest.scala"
public class ValueClassTest {
  public int x();
    Code:
       0: aload_0       
       1: getfield      #14                 // Field x:I
       4: ireturn       

  public void x_$eq(int);
    Code:
       0: aload_0       
       1: iload_1       
       2: putfield      #14                 // Field x:I
       5: return        

  public int y();
    Code:
       0: aload_0       
       1: getfield      #21                 // Field y:I
       4: ireturn       

  public void y_$eq(int);
    Code:
       0: aload_0       
       1: iload_1       
       2: putfield      #21                 // Field y:I
       5: return        

  public int m1(int);
    Code:
       0: iload_1       
       1: ireturn       

  public int m2(int);
    Code:
       0: iload_1       
       1: ireturn       

  public rklaehn.ValueClassTest();
    Code:
       0: aload_0       
       1: invokespecial #29                 // Method java/lang/Object."<init>":()V
       4: aload_0       
       5: iconst_1      
       6: putfield      #14                 // Field x:I
       9: aload_0       
      10: iconst_2      
      11: putfield      #21                 // Field y:I
      14: return        
}
10
votes

To extends this question, Wojciech Langiewicz proposes a nice example of Value class used as case class.

Instead of:

case class Player(id: Int, gameCash: Int, gameCoins: Int, energy: Int)

Wojciech defines:

case class Player(id: PlayerId, gameCash: GameCash, gameCoins: GameCoins, energy: Energy)

with the case classes (without allocation of additional objects on the heap):

case class PlayerId(id: Int) extends AnyVal
case class GameCash(value: Int) extends AnyVal
case class GameCoins(value: Int) extends AnyVal
case class Energy(value: Int) extends AnyVal

When creating case classes that wrap exactly one parameter you should add extends AnyVal to allow Scala compiler to run more optimizations – basically type checking will be done only during compilation phase, but during runtime only objects of the underlying type will be created which leads to less memory overhead.

Adding custom types in the specific points in our code not only improved readability but also allowed us to make less errors – offload some of the checking that otherwise would have to be done in tests (or not done at all) to the compiler. You can also immediately see errors in your IDE or editor.

Because now each component in the Player class is itself a separate type, it’s also very easy to add new operators that otherwise probably would have to be added implicitly which would pollute larger scope.

2
votes

At least for "allow creating-without-new-syntax" you can use plain old methods or objects with apply method. toString is also not a problem (if I remember correctly), although you must define it yourself if you aren't using case class.
BTW, language allows defining case classes which extend AnyVal, see http://docs.scala-lang.org/overviews/core/value-classes.html

1
votes

See the overview documentation section "When Allocation Is Necessary".

Case classes receive special notice: "Note: You can use case classes and/or extension methods for cleaner syntax in practice."

But, as @rudiger-klaehn already said, the caveat example is supplying an AnyVal where an Any is expected:

package anything

// the caveat from the overview
trait Distance extends Any
case class Meter(val value: Double) extends AnyVal with Distance

class Foo {
  def add(a: Distance, b: Distance): Distance = Meter(2.0)
}

object Test extends App {
  val foo = new Foo
  Console println foo.add(Meter(3.4), Meter(4.3))
}

Showing that :javap -app is fixed in the latest 2.11:

scala> :javap -app anything.Test$
  public final void delayedEndpoint$anything$Test$1();
    flags: ACC_PUBLIC, ACC_FINAL
    Code:
      stack=7, locals=1, args_size=1
         0: aload_0       
         1: new           #63                 // class anything/Foo
         4: dup           
         5: invokespecial #64                 // Method anything/Foo."<init>":()V
         8: putfield      #60                 // Field foo:Lanything/Foo;
        11: getstatic     #69                 // Field scala/Console$.MODULE$:Lscala/Console$;
        14: aload_0       
        15: invokevirtual #71                 // Method foo:()Lanything/Foo;
        18: new           #73                 // class anything/Meter
        21: dup           
        22: ldc2_w        #74                 // double 3.4d
        25: invokespecial #78                 // Method anything/Meter."<init>":(D)V
        28: new           #73                 // class anything/Meter
        31: dup           
        32: ldc2_w        #79                 // double 4.3d
        35: invokespecial #78                 // Method anything/Meter."<init>":(D)V
        38: invokevirtual #84                 // Method anything/Foo.add:(Lanything/Distance;Lanything/Distance;)Lanything/Distance;
        41: invokevirtual #88                 // Method scala/Console$.println:(Ljava/lang/Object;)V
        44: return        
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0      45     0  this   Lanything/Test$;
      LineNumberTable:
        line 11: 0
        line 12: 11
}

There is boxing as we were warned.

(Update: actually, the PR to fix -app isn't merged yet. Stay tuned.)