Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How are Scala traits compiled into Java bytecode?

Tags:

bytecode

scala

I have played around with Scala for a while now, and I know that traits can act as the Scala equivalent of both interfaces and abstract classes. How exactly are traits compiled into Java bytecode?

I found some short explanations that stated traits are compiled exactly like Java interfaces when possible, and interfaces with an additional class otherwise. I still don't understand, however, how Scala achieves class linearization, a feature not available in Java.

Is there a good source explaining how traits compile to Java bytecode?

like image 260
Justin Ardini Avatar asked Mar 31 '10 23:03

Justin Ardini


People also ask

Does Scala compile to bytecode?

The Scala compiler compiles to JVM bytecode. Just like the Java compiler also compiles to JVM bytecode.

Does Scala code compile to Java?

Although Scala compiles to Java bytecode, it is designed to improve on many of the perceived shortcomings of the Java language.

What is Scala trait in Java?

In scala, trait is a collection of abstract and non-abstract methods. You can create trait that can have all abstract methods or some abstract and some non-abstract methods. A variable that is declared either by using val or var keyword in a trait get internally implemented in the class that implements the trait.

How is bytecode compiled?

A compiler converts the source code to bytecode, an intermediary code that bridges the gap between the high-level source code and low-level machine code. The compiler is a special type of program that translates statements in the source code to bytecode, machine code or another programming language.


2 Answers

I'm not an expert, but here is my understanding:

Traits are compiled into an interface and corresponding class.

trait Foo {   def bar = { println("bar!") } } 

becomes the equivalent of...

public interface Foo {   public void bar(); }  public class Foo$class {   public static void bar(Foo self) { println("bar!"); } } 

Which leaves the question: How does the static bar method in Foo$class get called? This magic is done by the compiler in the class that the Foo trait is mixed into.

class Baz extends Foo 

becomes something like...

public class Baz implements Foo {   public void bar() { Foo$class.bar(this); } } 

Class linearization just implements the appropriate version of the method (calling the static method in the Xxxx$class class) according to the linearization rules defined in the language specification.

like image 162
Mitch Blevins Avatar answered Sep 21 '22 19:09

Mitch Blevins


For the sake of discussion, let's look the following Scala example using multiple traits with both abstract and concrete methods:

trait A {   def foo(i: Int) = ???   def abstractBar(i: Int): Int }  trait B {   def baz(i: Int) = ??? }  class C extends A with B {   override def abstractBar(i: Int) = ??? } 

At the moment (i.e. as of Scala 2.11), a single trait is encoded as:

  • an interface containing abstract declarations for all the trait's methods (both abstract and concrete)
  • an abstract static class containing static methods for all the trait's concrete methods, taking an extra parameter $this (in older versions of Scala, this class wasn't abstract, but it doesn't make sense to instantiate it)
  • at every point in the inheritance hierarchy where the trait is mixed in, synthetic forwarder methods for all the concrete methods in the trait that forward to the static methods of the static class

The primary advantage of this encoding is that a trait without concrete members (which is isomorphic to an interface) actually is compiled to an interface.

interface A {     int foo(int i);     int abstractBar(int i); }  abstract class A$class {     static void $init$(A $this) {}     static int foo(A $this, int i) { return ???; } }  interface B {     int baz(int i); }  abstract class B$class {     static void $init$(B $this) {}     static int baz(B $this, int i) { return ???; } }  class C implements A, B {     public C() {         A$class.$init$(this);         B$class.$init$(this);     }      @Override public int baz(int i) { return B$class.baz(this, i); }     @Override public int foo(int i) { return A$class.foo(this, i); }     @Override public int abstractBar(int i) { return ???; } } 

However, Scala 2.12 requires Java 8, and thus is able to use default methods and static methods in interfaces, and the result looks more like this:

interface A {     static void $init$(A $this) {}     static int foo$(A $this, int i) { return ???; }     default int foo(int i) { return A.foo$(this, i); };     int abstractBar(int i); }  interface B {     static void $init$(B $this) {}     static int baz$(B $this, int i) { return ???; }     default int baz(int i) { return B.baz$(this, i); } }  class C implements A, B {     public C() {         A.$init$(this);         B.$init$(this);     }      @Override public int abstractBar(int i) { return ???; } } 

As you can see, the old design with the static methods and forwarders has been retained, they are just folded into the interface. The trait's concrete methods have now been moved into the interface itself as static methods, the forwarder methods aren't synthesized in every class but defined once as default methods, and the static $init$ method (which represents the code in the trait body) has been moved into the interface as well, making the companion static class unnecessary.

It could probably be simplified like this:

interface A {     static void $init$(A $this) {}     default int foo(int i) { return ???; };     int abstractBar(int i); }  interface B {     static void $init$(B $this) {}     default int baz(int i) { return ???; } }  class C implements A, B {     public C() {         A.$init$(this);         B.$init$(this);     }      @Override public int abstractBar(int i) { return ???; } } 

I'm not sure why this wasn't done. At first glance, the current encoding might give us a bit of forwards-compatibility: you can use traits compiled with a new compiler with classes compiled by an old compiler, those old classes will simply override the default forwarder methods they inherit from the interface with identical ones. Except, the forwarder methods will try to call the static methods on A$class and B$class which no longer exist, so that hypothetic forwards-compatibility doesn't actually work.

like image 25
Jörg W Mittag Avatar answered Sep 22 '22 19:09

Jörg W Mittag