By: Martin Odersky and Jeff Olson and Paul Phillips and Joshua Suereth
Note from the SIP Committee: we think future SIP(s), using work from SIP-15, can provide more benefit to numerical computing users. The SIP as it exists benefits all users of implicit enrichment classes, and takes us much further to unboxed high performance code. This SIP does not exclude further work towards improving numerical computing in Scala.
History
| Date | Version |
|---|---|
| Jan 30th 2012 | Original inline classes proposal |
| Feb 7th 2012 | Changed inline classes to value classes, added Josh Suereth as author. |
Introduction
This is a proposal to introduce syntax for classes in Scala that can get completely inlined, so operations on these classes have zero overhead compared to external methods. Some use cases for inlined classes are:
- Inlined implicit wrappers. Methods on those wrappers would be translated to extensions methods.
- New numeric classes, such as unsigned ints. There would no longer need to be a boxing overhead for such classes. So this is similar to value types in .NET.
- Classes representing units of measure. Again, no boxing overhead would be incurred for these classes.
The proposal is currently in an early stage. It’s not yet been implemented, and the proposed implementation strategy is too complicated to be able to predict with certainty that it will work as specified. Consequently, details of the proposal might change driven by implementation concerns.
Value Classes
The gist of the proposal is to allow user-defined classes to extend from AnyVal in situations like this:
class C (val u: U) extends AnyVal {
def m1(ps1) = ...
...
def mN(psN) = ...
}
Such classes are called value classes. A value class C must satisfy the
following criteria:
Cmust have exactly one parameter, which is marked withvaland which has public accessibility. The type of that parameter (e.g.Uabove) is called the underlying type ofCCmay not have@specializedtype parameters.- The underlying type of
Cmay not be a value class. Cmay not have secondary constructors.Cmay not define concreteequalsorhashCodemethods.Cmust be either a toplevel class or a member of a statically accessible object.Cmust be ephemeral.
A class or trait C is ephemeral if the following holds:
Cmay not declare fields (other than the parameter of a value class).Cmay not contain object definitions.Cmay not have initialization statements.
We say that a value class C unboxes directly to a class D if the
underlying type of C is a type-instance of D. Indirect unboxing is the
transitive closure of direct unboxing. A value class may not unbox directly or
indirectly to itself.
The following implicit assumptions apply to value classes.
- Value classes are implicitly treated as final, so they cannot be extended by other classes.
-
Value classes are implicitly assumed to have structural equality and hash codes. I.e. their
equalsandhashCodemethods are taken to be defined as follows:def equals(other: Any) = other match { case that: C => this.u == that.u case _ => false } def hashCode = u.hashCode
Universal traits
Scala’s rule for inheritance do not permit value classes to extend traits that
extend from AnyRef. To permit value classes to extend traits, we introduce
universal traits, which extend from Any. A universal trait T needs to
explicitly extend class Any. In the example below, Equals is a universal
trait with superclass Any, but Ordered’s superclass is still assumed to be
AnyRef.
trait Equals[T] extends Any { ... }
trait Ordered[T] extends Equal[T] { ... }
To turn Ordered into a universal trait, add an explicit superclass Any:
trait Ordered[T] extends Any with Equal[T] { ... }
Like value classes, universal traits need to be ephemeral.
Expansion of value classes.
Value classes are expanded as follows. For concreteness, we assume a value class
Meter that is defined like this:
class Meter(val underlying: Double) extends AnyVal with Printable {
def plus (other: Meter): Meter =
new Meter(this.underlying + other.underlying)
def divide (factor: Double): Meter = new Meter(this.underlying / factor)
def less (other: Meter): Boolean = this.underlying < other.underlying
override def toString: String = underlying.toString + “m”
}
For simplicity we assume that all expansion steps are done on erased types.
Step 1: Extracting methods.
Let the extractable methods of a value class be all methods that are directly
declared in the class (as opposed to being inherited) and that do not contain a
super call in their body. For each extractable method m, we create another
method named extension$m in the companion object of that class (if no
companion object exists, a fresh one is created). The extension$m method
takes an additional parameter in first position which is named $this and has
the value class as type. Generally, in a value class
class C(val u: U) extends AnyVal
a method
def m(params): R = body
is expanded to the following method in the companion object of class C:
def extension$m($this: C, params): R = body2
Here body2 is the same as body with each occurrence of this or C.this
replaced by $this. The original method m in C will be changed to
def m(params): R = C.extension$m(this, params)
Overloaded methods may be augmented with an additional integer to distinguish
them after types are erased (see the transformations of the divide method in
the following steps).
Also in this step, synthetic hashCode and equals methods are added to the
class.
In our example, the Meter class would be expanded as follows:
class Meter(val underlying: Double) extends AnyVal with Printable {
def plus (other: Meter): Meter =
Meter.extension$plus(this, other)
def divide (other: Meter): Double =
Meter.extension1$divide(this, other)
def divide (factor: Double): Meter =
Meter.extension2$divide(this, factor)
def less (other: Meter): Boolean =
Meter.extension$less(this, other)
override def toString: String =
Meter.extension$toString(this)
override def equals(other: Any) =
Meter.extension$equals(this, other)
override def hashCode =
Meter.extension$hashCode(this)
}
object Meter {
def extension$plus($this: Meter, other: Meter) =
new Meter($this.underlying + other.underlying)
def extension1$divide($this: Meter, other: Meter): Double =
$this.underlying / other.underlying
def extension2$divide($this: Meter, factor: Double): Meter =
new Meter($this.underlying / factor)
def extension$less($this: Meter, other: Meter): Boolean =
$this.underlying < other.underlying
def extension$toString($this: Meter): String =
$this.underlying.toString + “m”
def extension$equals($this: Meter, other: Any) = other match {
case that: Meter => $this.underlying == that.underlying
case _ => false
}
def extension$hashCode($this: Meter) = $this.underlying
}
Step 2: Rerouting calls
In this step any call to a method that got extracted in step 1 into a companion object gets redirected to the newly created method in that companion object. Generally, a call
p.m(args)
where m is an extractable method declared in a value class C gets rewritten to
C.extension$m(p, args)
For instance the two calls in the following code fragment
val x, y: Meter
x.plus(y)
x.toString
would be rewritten to
Meter.extension$plus(x, y)
Meter.extension$toString(x)
Step 3: Erasure
Next, we introduce for each value class C a new type C$unboxed (this type
will be eliminated again in step 4). The newly generated type is assumed to have
no members and to be completely outside the normal Scala class hierarchy. That
is, it is a subtype of no other type and is a supertype only of scala.Nothing.
We now replace every occurrence of the type C in a symbol’s type or in a tree’s
type annotation by C$unboxed. There are however the following two exceptions
to this rule:
-
Type tests are left unaffected. So, in the type test below,
Cis left as it is.e.isInstanceOf[C] -
All occurrences of methods in class
Care left unaffected.
We then re-typecheck the program, performing the following adaptations if types do not match up.
-
If
eis an expression of typeC$unboxed, and the expected type is some other typeT,eis converted to typeCusingnew C(e.asInstanceOf[U])where
Uis the underlying type ofC. After that, further adaptations may be effected onC, employing the usual rules of erasure typing. Similarly, if a selection is performed on an expression of typeC$unboxed, the expression is first converted to typeCusing the conversion above. -
If the expected type of an expression
eof typeTisC$unboxed, theneis first adapted with expected typeCgivinge2, ande2then is converted toC$unboxedusinge2.u.asInstanceOf[C$unboxed]where
uis the name of the value parameter ofC. Similarly, if an expressioneis explicitly converted usinge.asInstanceOf[C$unboxed]then
eis first converted to typeC, givinge2, and the cast is then replaced bye2.u.asInstanceOf[C$unboxed] -
The rules for conversions from and to arrays over value classes are analogous to the rules for arrays over primitive value classes.
Value classes are rewritten at this stage to normal reference classes. That is,
their parent changes from AnyVal to java.lang.Object. The AnyVal type
itself is also rewritten during erasure to java.lang.Object, so the change
breaks no subtype relationships.
We finally perform the following peephole optimizations:
new C(e).u ==> e
new C(e).isInstanceOf[C] ==> true
new C(e) == new C(f) ==> e == f
new C(e) != new C(f) ==> e != f
Step 4: Cleanup
In the last step, all occurrences of type C$unboxed are replaced by the
underlying type of C. Any redundant casts of the form
e.asInstanceOf[T]
where e is already of type T are removed and replaced by e.
Examples
Example 1
The program statements on the left are converted using steps 1 to 3 to the statements on the right.
var m, n: Meter var m, n: Meter$unboxed
var o: AnyRef var o: AnyRef
m = n m = n
o = m o = new Meter(m.asInstanceOf[Double])
m.print new Meter(m.asInstanceOf[Double]).print
m less n Meter.extension$less(m, n)
m.toString Meter.extension$toString(m)
m.isInstanceOf[Ordered] new Meter(m.asInstanceOf[Double]).isInstanceOf[Ordered]
m.asInstanceOf[Ordered] new Meter(m.asInstanceOf[Double]).asInstanceOf[Ordered]
o.isInstanceOf[Meter] o.isInstanceOf[Meter]
o.asInstanceOf[Meter] o.asInstanceOf[Meter].underlying.asInstanceOf[Meter$unboxed]
m.isInstanceOf[Meter] new Meter(m.asInstanceOf[Double]).isInstanceOf[Meter]
m.asInstanceOf[Meter] m.asInstanceOf[Meter$unboxed]
Including the cleanup step 4 the same program statements are converted as follows.
var m, n: Meter var m, n: Double
var o: Any var o: Any
m = n m = n
o = m o = new Meter(m)
m.print new Meter(m).print
m less n Meter.extension$less(m, n)
m.toString Meter.extension$toString(m)
m.isInstanceOf[Ordered] new Meter(m).isInstanceOf[Ordered]
m.asInstanceOf[Ordered] new Meter(m).asInstanceOf[Ordered]
o.isInstanceOf[Meter] o.isInstanceOf[Meter]
o.asInstanceOf[Meter] o.asInstanceOf[Meter].underlying
m.isInstanceOf[Meter] new Meter(m).isInstanceOf[Meter]
m.asInstanceOf[Meter] m.asInstanceOf[Double]
Example 2
After all 4 steps the Meter class is translated to the following code.
class Meter(val underlying: Double) extends AnyVal with Printable {
def plus (other: Meter): Meter =
new Meter(Meter.extension$plus(this.underlying, other.underlying))
def divide (other: Meter): Double =
Meter.extension1$divide(this.underlying, other)
def divide (factor: Double): Meter =
new Meter(Meter.extension2$divide(this.underlying, factor))
def less (other: Meter): Boolean =
Meter.extension$less(this.underlying, other)
override def toString: String =
Meter.extension$toString(this.underlying)
override def equals(other: Any) =
Meter.extension$equals(this, other)
override def hashCode =
Meter.extension$hashCode(this)
}
object Meter {
def extension$plus($this: Double, other: Double) =
$this + other
def extension1$divide($this: Double, other: Double): Double =
$this / other
def extension2$divide($this: Double, factor: Double): Double =
$this / factor)
def extension$less($this: Double, other: Double): Boolean =
$this < other
def extension$toString($this: Double): String =
$this.toString + “m”
def extension$equals($this: Double, other: Object) = other match {
case that: Meter => $this == that.underlying
case _ => false
}
def extension$hashCode($this: Double) = $this.hashCode
}
Note that the two divide methods end up with the same type in object Meter.
(The fact that they also have the same body is accidental). That’s why we needed
to distinguish them by adding an integer number.
The same situation can arise in other circumstances as well: Two overloaded methods might end up with the same type after erasure. In the general case, Scala would treat this situation as an error, as it would for other types that get erased. So we propose to solve only the specific problem that multiple overloaded methods in a value class itself might clash after erasure.
Further Optimizations?
The proposal foresees that only methods defined directly in a value class get expanded in the companion object; methods inherited from universal traits are unaffected. For instance, in the example above
m.print
would translate to
new Meter(m).print
We might at some point want to investigate ways how inherited trait methods can also be inlined. For the moment this is outside the scope of the proposal.