Search code examples
scalavariadic-functionsvariadic

What do variadic functions get compiled to?


In Java, variadic methods are re-written by the compiler so that they become methods that take an array where the variadic arguments are expected (as per this answer).

What happens in Scala?

My main concern is whether the variadic arguments are implicitly copied to an Array if another type of collection is passed or not, i.e. would the compiler somehow re-write this snippet:

val strings = Seq("hello")
"%s".format(strings: _*)

to the following?

val strings = Seq("hello")
"%s".format(strings.toArray: _*)

As a follow-up question: are there differences whether the variadic method implements a Java interface or if it's pure Scala?


Solution

  • You can check this out pretty easily with javap -v. If you compile the following code (using String.format instead of "%s".format for now) with 2.12:

    class Example1 {
      val strings = Seq("foo")
      def formatResult = String.format("%s", strings: _*)
    }
    

    You'll get this:

      public java.lang.String formatResult();
        descriptor: ()Ljava/lang/String;
        flags: ACC_PUBLIC
        Code:
          stack=4, locals=1, args_size=1
             0: ldc           #21                 // String %s
             2: aload_0
             3: invokevirtual #23                 // Method strings:()Lscala/collection/Seq;
             6: getstatic     #29                 // Field scala/reflect/ClassTag$.MODULE$:Lscala/reflect/ClassTag$;
             9: ldc           #31                 // class java/lang/String
            11: invokevirtual #35                 // Method scala/reflect/ClassTag$.apply:(Ljava/lang/Class;)Lscala/reflect/ClassTag;
            14: invokeinterface #41,  2           // InterfaceMethod scala/collection/Seq.toArray:(Lscala/reflect/ClassTag;)Ljava/lang/Object;
            19: checkcast     #43                 // class "[Ljava/lang/Object;"
            22: invokestatic  #47                 // Method java/lang/String.format:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
            25: areturn
          LineNumberTable:
            line 3: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      26     0  this   LExample1;
    

    So yep, it'll convert strings to an array.

    If you use "%s".format, though, like this:

    class Example2 {
      val strings = Seq("foo")
      def formatResult = "%s".format(strings: _*)
    }
    

    You won't see the conversion:

      public java.lang.String formatResult();
        descriptor: ()Ljava/lang/String;
        flags: ACC_PUBLIC
        Code:
          stack=4, locals=1, args_size=1
             0: new           #21                 // class scala/collection/immutable/StringOps
             3: dup
             4: getstatic     #27                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
             7: ldc           #29                 // String %s
             9: invokevirtual #33                 // Method scala/Predef$.augmentString:(Ljava/lang/String;)Ljava/lang/String;
            12: invokespecial #37                 // Method scala/collection/immutable/StringOps."<init>":(Ljava/lang/String;)V
            15: aload_0
            16: invokevirtual #39                 // Method strings:()Lscala/collection/Seq;
            19: invokevirtual #43                 // Method scala/collection/immutable/StringOps.format:(Lscala/collection/Seq;)Ljava/lang/String;
            22: areturn
          LineNumberTable:
            line 14: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      23     0  this   LExample2;
    

    This is because the Scala compiler's encoding of varargs is different from the Java compiler's (which of course knows nothing about Scala's Seq). You can force the Scala compiler to generate Java-compatible varargs methods with the @varargs annotation:

    class Example3 {
      def foo(xs: String*): Unit = ()
    
      @annotation.varargs
      def bar(xs: String*): Unit = ()
    
      val strings = Seq("foo")
      def fooResult = foo(strings: _*)
      def barResult = bar(strings: _*)
    }
    

    Note that this generates both encodings, though, so bar(strings: _*) still won't involve an array conversion, since the Scala compiler in that case chooses the Scala-encoded method.

    So to sum up: calling Java varargs methods from Scala with seq: _* will always involve toArray getting called on the Seq, while this won't happen when calling Scala varargs methods from Scala (whether or not they're annotated with @varargs for Java compatibility).