Search code examples
scalatuplesiterable-unpacking

Why does Scala construct a new Tuple when unpacking a Tuple?


Why does this Scala code:

class Test
{
  def foo: (Int, String) =
  {
    (123, "123")
  }

  def bar: Unit =
  {
    val (i, s) = foo
  }
}

generate the following bytecode for bar() that constructs a new Tuple2, passes the Tuple2 from foo() to it and then gets the values out of it?

public void bar();
Code:
 0:   aload_0
 1:   invokevirtual   #28; //Method foo:()Lscala/Tuple2;
 4:   astore_2
 5:   aload_2
 6:   ifnull  40
 9:   new     #7; //class scala/Tuple2
 12:  dup
 13:  aload_2
 14:  invokevirtual   #32; //Method scala/Tuple2._1:()Ljava/lang/Object;
 17:  aload_2
 18:  invokevirtual   #35; //Method scala/Tuple2._2:()Ljava/lang/Object;
 21:  invokespecial   #20; //Method scala/Tuple2."<init>":(Ljava/lang/Object;Ljava/lang/Object;)V
 24:  astore_1
 25:  aload_1
 26:  invokevirtual   #39; //Method scala/Tuple2._1$mcI$sp:()I
 29:  istore_3
 30:  aload_1
 31:  invokevirtual   #35; //Method scala/Tuple2._2:()Ljava/lang/Object;
 34:  checkcast       #41; //class java/lang/String
 37:  astore  4

Is this because the compiler isn't checking that foo()s return value isn't a tuple?

Will the JVM optimize the construction away anyway?


Solution

  • This seems to be according to the spec (in 4.1 Value Declarations and Definitions - slightly reformatted for stackoverflow display):

    Value definitions can alternatively have a pattern (§8.1) as left-hand side. If p is some pattern other than a simple name or a name followed by a colon and a type, then the value definition val p = e is expanded as follows:

    1. If the pattern p has bound variables x1, . . . , xn, where n >= 1: Here, $x is a fresh name.
      val $x = e match {case p => (x1, . . . , xn)}
      val x1 = $x._1
      . . .
      val xn = $x._n
    

    So the tuple creation happens in the parser phase. So val (i, s) = (1, "s") expends at the end of the parser phase to:

    private[this] val x$1 = scala.Tuple2(1, "s"): @scala.unchecked match {    
      case scala.Tuple2((i @ _), (s @ _)) => scala.Tuple2(i, s)
    };
    val i = x$1._1;
    val s = x$1._2
    

    Measuring this on this simple test on a million iterations:

    def foo: (Int, String) = (123, "123")
    def bar: Unit = { val (i, s) = foo }
    def bam: Unit = { val f = foo; val i = f._1; val s = f._2 }
    

    yields

    foo: Elapsed: 0.030
    bar: Elapsed: 0.051
    ._1 ._2 access: Elapsed: 0.040
    

    and with -optimize flag:

    foo: Elapsed: 0.027
    bar: Elapsed: 0.049
    ._1 ._2 access: Elapsed: 0.029