About Me

Hello, I'm an eclectic software professional working with enterprise software at SAP. This blog only contains my personal views, thoughts and opinions. It is not endorsed by SAP nor does it constitute any official communication of SAP.

Wednesday, March 10, 2010

Scala Traits Under the Hood

Scala is a very feature-rich language built on top of the JVM (there's also a version on the CLR, but the focus is clearly on the JVM). Besides its functional features, it introduces a number of constructs that facilitate large scale components development and code reuse. Scala has traits, which are a great tool for orthogonal code reuse.

In its simplest form, a trait can be used just like an interface, with only abstract members, defining a contract that derived types must fulfill. It can also have concrete members, including properties and fields. Classes (and traits) can inherit from multiple traits ("inherit" is the wrong term here, better ones are "use" or "mix in"), and gain all their capabilities. Traits enable a kind of composition that doesn't rely on the structure imposed by a multiple inheritance relationship. It's more suitable for combining a target class with the roles that it performs, which should not be part of its inheritance tree. These roles are normally small, focused and reusable in different contexts.

Here's a naive example, inspired by the DCI architecture idea:

import scala.collection.mutable._

trait Journal[A] { // A is a type parameter
  private val _log = new ListBuffer[A] // val defines a read-only property
  def Entries = _log.elements // def defines a method
  def AppendLog(entry: A) = {
    _log += entry // appends entry to the _log list
  }
}

trait TransferSource extends Journal[String] {
  def Withdraw(amount: Long): Unit // Unit is the same as void
  def TransferTo(recipient: TransferTarget, amount: Long): Unit = {
    Withdraw(amount)
    recipient.Deposit(amount)
    LogWithdraw(amount)
    recipient.LogDeposit(amount)
  }
  def LogWithdraw(amount: Long): Unit = {
    AppendLog("Withdrew: " + amount)
  }
}

trait TransferTarget extends Journal[String] {
  def Deposit(amount: Long): Unit
  def TransferFrom(source: TransferSource, amount: Long): Unit = {
    source.TransferTo(this, amount)
  }
  def LogDeposit(amount: Long): Unit = {
    AppendLog("Deposited: " + amount)
  }
}

class Account(balance: Long) // balance is a constructor parameter
  extends TransferSource with TransferTarget {
  private var _balance = balance
  def Balance = _balance
  def Withdraw(amount: Long): Unit = {
    if (amount > Balance) throw new RuntimeException("Insufficient funds")
    _balance -= amount
  }
  def Deposit(amount: Long): Unit = {
    _balance += amount
  }
}

This code defines three traits and a class. The first trait is Journal, which is a simple log of events, with a private field for the journal entries and two methods that access it. Then, the TransferSource and TransferTarget traits define the dual roles that a financial transaction has, the debitor and the creditor. These traits use Journal to do their bookkeeping. The methods Withdraw and Deposit are abstract, because they have no implementation. The class Account combines both transfer traits and provides the necessary implementations for their abstract members.

You can use it like this:

val savingsStartBalance = 10000
val checkingStartBalance = 500

val savings = new Account(savingsStartBalance)
val checking = new Account(checkingStartBalance)

savings.TransferFrom(checking, 100)
checking.TransferTo(savings, 50)
savings.TransferTo(checking, 10)

println("** Savings **")
println("Start: " + savingsStartBalance)
savings.Entries foreach { println(_) }
println("Balance: " + savings.Balance)

println()

println("** Checking **")
println("Start: " + checkingStartBalance)
checking.Entries foreach { println(_) }
println("Balance: " + checking.Balance)

Which will print:

** Savings **
Start: 10000
Deposited: 100
Deposited: 50
Withdrew: 10
Balance: 10140

** Checking **
Start: 500
Withdrew: 100
Withdrew: 50
Deposited: 10
Balance: 360

Scala effectively implements traits in a single-inheritance platform. How does it do this?

Compiling this code to .NET and then decompiling it to C# yields the following (adapted) code for the Journal trait:

// WARNING: some invalid C# identifiers ahead (they make sense only in the CIL)
public interface Journal {
  // methods in the original trait
  void AppendLog(object obj); // type parameters are erased
  object Entries();
  
  // private val _log --> became a getter and a setter:
  ListBuffer Journal$$_log();
  void Journal$$_log_$eq(ListBuffer lb);
}

public abstract class Journal$class {
  public static void $init$(Journal $this) {
    $this.Journal$$_log_$eq(new ListBuffer()); // initializes the _log
  }

  public static void AppendLog(Journal $this, object entry) {
    // $this._log += entry
    $this.Journal$$_log().$plus$eq(entry);
  }

  public static object Entries(Journal $this) {
    // $this._log.elements
    return $this.Journal$$_log().elements();
  }
}

The trait is split into an interface and an auxiliary class. The interface defines all methods that were on the trait, and a getter and a setter for the original _log field. The auxiliary class calls the setter to initialize it to a new list instance on its $init$ method. The helper class also contains the concrete methods that were on the trait, turned into static methods that take as a first parameter the interface type ($this). This eerily resembles the C# quasi-mixin pattern. The other traits go through similar transformations. When a trait uses other traits, its interface will inherit from the other traits interfaces:

public interface TransferTarget : Journal { 
  ... 
}

public interface TransferSource : Journal { 
  ...
}

The Account class, then, becomes this:

// WARNING: some invalid C# identifiers ahead (they make sense only in the CIL)
public class Account : 
  Journal, TransferSource, TransferTarget { // implements the used traits interfaces
  
  public Account(long balance) {
    this._balance = balance;
    // calls $init$ on the used traits helper classes
    Journal$class.$init$(this);
    TransferSource$class.$init$(this);
    TransferTarget$class.$init$(this);
  }

  // Journal trait interface implementation

  public virtual void AppendLog(object x$1) {
    Journal$class.AppendLog(this, x$1);
  }

  public virtual object Entries() {
    return Journal$class.Entries(this);
  }

  // backing field for Journal._log
  private ListBuffer Journal$$_log;

  // getter and setter for Journal._log
  public virtual ListBuffer Journal$$_log() {
    return this.Journal$$_log;
  }
  public virtual void Journal$$_log_$eq(ListBuffer x$1) {
    this.Journal$$_log = x$1;
  }

  // ... other traits interfaces implementations ...

  public virtual void TransferFrom(TransferSource x$1, long x$2) {
    TransferTarget$class.TransferFrom(this, x$1, x$2);
  }

  public virtual void TransferTo(TransferTarget x$1, long x$2) {
    TransferSource$class.TransferTo(this, x$1, x$2);
  }

  // ... other Account members ...

  public virtual void Withdraw(long amount) {
    if (amount > this.Balance()) {
      throw new Exception("Insufficient funds");
    }
    this._balance_$eq(this._balance() - amount);
  }
}

Now we can see many of the transformations that the Scala compiler performs to make traits possible. Account implements all three traits interfaces, and the compiler injects a few things to make it possible:

  • Backing fields for all traits fields and properties.
  • Getter and setter methods that implement the traits properties (and fields) and that use the backing fields.
  • The constructor calls the traits initialization methods.
  • Traits concrete methods are created, and they just delegate to the corresponding method on the trait helper class. The class author is still responsible for implementing the traits abtract methods, if they're not provided by other used traits.

There's also an involved algorithm to resolve complex inheritance scenarios. The traits in a complex hierarchy get linearized into its target types. As such, a trait appears at most once in an inheritance tree, like Journal in the example. This makes complex overriding and virtual parents scenarios possible.

No comments:

Post a Comment