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:
C
must have exactly one parameter, which is marked withval
and which has public accessibility. The type of that parameter (e.g.U
above) is called the underlying type ofC
C
may not have@specialized
type parameters.- The underlying type of
C
may not be a value class. C
may not have secondary constructors.C
may not define concreteequals
orhashCode
methods.C
must be either a toplevel class or a member of a statically accessible object.C
must be ephemeral.
A class or trait C
is ephemeral if the following holds:
C
may not declare fields (other than the parameter of a value class).C
may not contain object definitions.C
may 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
equals
andhashCode
methods 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,
C
is left as it is.e.isInstanceOf[C]
-
All occurrences of methods in class
C
are left unaffected.
We then re-typecheck the program, performing the following adaptations if types do not match up.
-
If
e
is an expression of typeC$unboxed
, and the expected type is some other typeT
,e
is converted to typeC
usingnew C(e.asInstanceOf[U])
where
U
is 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 typeC
using the conversion above. -
If the expected type of an expression
e
of typeT
isC$unboxed
, thene
is first adapted with expected typeC
givinge2
, ande2
then is converted toC$unboxed
usinge2.u.asInstanceOf[C$unboxed]
where
u
is the name of the value parameter ofC
. Similarly, if an expressione
is explicitly converted usinge.asInstanceOf[C$unboxed]
then
e
is 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.