Skip to content

Commit 9daa075

Browse files
committed
Add draft proposal
1 parent cb7aa84 commit 9daa075

File tree

2 files changed

+303
-9
lines changed

2 files changed

+303
-9
lines changed
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
# Specialized Traits
2+
3+
Specialization is one of the few remaining desirable features from Scala 2 that's are as yet missing in Scala 3. We could try to port the Scala 2 scheme, which would be non-trivial since the implementation is quite complex. But that scheme is problematic enough to suggest that we also look for alternatives. A possible alternative is described here. It is meant to complement the [proposal on inline traits](https://github.com/lampepfl/dotty/issues/15532). That proposal also contains a more detailed critique of Scala 2 specialization.
4+
5+
The main problem of Scala-2 specialization is code bloat. We have to pro-actively generate up to 11 copies of functions and classes when they have a specialized type parameter, and this grows exponentially with the number of such type parameters. Miniboxing tries to reduce the number under the exponent from ~10 to 3 or 4, but it has problems dealing with arrays.
6+
7+
Languages like C++, Rust, Go, D, or Zig avoid the proactive generation of all possible specializations by monomorphizing the whole program. This means we only need to generate a specialized version of a function or class if it is actually used in the program. On the other hand, a global monomorphization can lead itself to code bloat and long compile times. It is also a problematic choice for binary APIs.
8+
9+
This note discusses a different scheme to get specialization for Scala 3, which is somewhat between Scala 2's selective specialization and full monomorphization. As in Scala 2, specialized type parameters are tagged explicitly (but not with an annotation). But as for monomorphization, specializations are only generated if a specialized type is referenced in the program. To make this work efficiently, we need a way to transport information about possible specialization types through generic code (full monomorphization does not need that since it eliminates all generic code).
10+
11+
We do that using a type class `Specialized` that is typically used as a context bound on a type parameter of some class. It indicates that we want to create specialized versions of that class where the type parameter is instantiated to the type argument. The specialized versions offer optimization opportunities compared to the generic class.
12+
13+
## Example
14+
15+
As a first example, consider a `Vec` trait for vectors over a numeric type.
16+
```scala
17+
import scala.math.Numeric
18+
19+
inline trait Vec[T: {Specialized, Numeric}](elems: Array[T]):
20+
21+
def length = elems.length
22+
23+
def apply(i: Int): T = elems(i)
24+
25+
def scalarProduct(other: Vec[T]): T =
26+
require(this.length == other.length)
27+
var result = num.fromInt(0)
28+
for i <- 0 until length do
29+
result = num.plus(result, num.times(this(i), other(i)))
30+
result
31+
32+
object Vec:
33+
inline def apply[T: Specialized](elems: Array[T]) = new Vec[T](elems) {}
34+
end Vec
35+
```
36+
The idea is that we want to specialize vectors on the type parameter `T` in order to get important efficiency gains, including the following:
37+
38+
- Use an array `arr` specialized to the actual element instead of a fully generic array that has to be accessed via reflection
39+
- Avoid boxing for internal values like `result`
40+
- Avoid boxing in the API for values like the result of `scalarProduct`
41+
- Specialize on the concrete `Numeric` class instance for `T`, so that calls to `num`'s methods have static targets and can be inlined.
42+
43+
## Terminology and Restrictions
44+
45+
A _specialized trait_ is an inline trait that has at least one `Specialized` context bound.
46+
47+
A specialized context bound (or its expansion to a context parameter) is only allowed for
48+
type parameters of inline methods and inline traits. Regular methods or traits or classes
49+
cannot take `Specialized[T]` parameters. Hence, the only way to create a specialized trait is using an anonymous class instance, like in the `Vec.apply` method above. What's more,
50+
we require that each such anonymous class instance
51+
52+
- can extend only a single specialized trait,
53+
- cannot mix in further classes or traits, and
54+
- cannot contain member definitions.
55+
56+
So each such class instance is of the form `new A[Ts](ps1)...(psN) {}` where
57+
`A` is a specialized trait and the type parameters `Ts` and term parameters `ps1, ,,, psN` which can also be absent.
58+
59+
The restrictions ensure that each time we create an instance of a specialized trait we know statically the classes of all `Specialized` type arguments. This enables us to implement the following expansion scheme:
60+
61+
62+
## Expansion of Specialized Traits
63+
64+
A type instance of a specialized trait such as Vec[Tp] has a special erasure, which depends on the specializing supertype of `Tp`.
65+
66+
**Definition**: A _simple class type_ is a reference to a static class that does not have type parameters. References to traits and references containing non-static prefixes or refinements are excluded.
67+
68+
**Definition**: A top class is one of `Any`, `AnyVal`, or `Object`.
69+
70+
**Definition**: The _specializing supertype_ `SpecType(Tp)` of a type `Tp` is the smallest simple class type `C` such that
71+
72+
- `Tp` is a subtype of `C`
73+
- The superclass of `C` is a top class, or `C` itself is a top class.
74+
75+
The _erasure_ of `Vec[Tp]` where `SpecType(Tp) = C` is:
76+
77+
- If `C` is one of the top classes `Any` or `AnyRef` or `AnyVal`, the usual erased trait `Vec`.
78+
- If `C` is some other class, a new specialized instance trait with a name of the form `Vec$sp$TN`,
79+
where `$sp$` is a fixed specialization marker and `TN` is an encoding of the fully qualified name of `C`.
80+
81+
If there is more than one specialized type parameter, the specialized instance trait will reflect in its name all specializing supertypes of such type parameters in sequence.
82+
83+
An anonymous class instance creation like `new Vec[T](elems) {}` expands to
84+
an instance creation `new Vec$impl$TN(elems)` of a new _specialized instance class_
85+
named `Vec$impl$TN`. Here, `Vec$sp$TN` is the erasure of `Vec[T]` and the class name derives from that trait name by replacing `$sp` with `$impl$`.
86+
87+
88+
The specialized instance traits are created on demand the first time they are mentioned in a type. For example, here is the definition of the specialized instance `Vec$sp$Int` for `Vec[Int]`:
89+
90+
```scala
91+
trait Vec$sp$Int extends Vec[Int]:
92+
def length: Int
93+
def apply(x: Int): Int
94+
def scalarProduct(other: Vec[T]): Int
95+
```
96+
97+
In general a specialized instance trait that specializes an inline trait `A[T]` with a specialization type `S`:
98+
99+
- drops all specialized trait parameters of `A`,
100+
- adds `A[S]` as first parent trait,
101+
- _also_ adds all parents of `A` in their specialized forms,
102+
- contains all specialized declarations of `A`.
103+
104+
A specialized instance class for an inline trait `A` at specialized argument `S`
105+
106+
- repeats the value parameters of trait `A`,
107+
- extends `A[S]`.
108+
109+
For example, here is the specialized instance class for `Vec` at `Int`:
110+
111+
```scala
112+
class Vec$impl$Int(elems: Array[T]) extends Vec[Int]
113+
```
114+
115+
After inlining `Vec[Int]` the expanded class looks like this:
116+
```scala
117+
class Vec(elems: Array[Int])(using Numeric[Int]):
118+
119+
def length: Int = elems.length
120+
def apply(i: Int): Int = elems(i)
121+
122+
def scalarProduct(other: Vec[Int]): Int =
123+
require(this.length == other.length)
124+
var result = num.fromInt(0)
125+
for i <- 0 until length do
126+
result = num.plus(result, num.times(this(i), other(i)))
127+
result
128+
```
129+
130+
More examples of expansions are shown in the case study below.
131+
132+
## Caching of Specialized Traits and Classes
133+
134+
To avoid redundant repeated code generation of the same traits and classes, specialized instance traits and classes are cached. The compiler will put their tasty and classfile artifacts in a special directory
135+
on the class path. Each artifact will contain in an annotation a hash of the contents of the trait from which the instance was derived. Before creating a new specialized instance, the compiler will consult this directory to see whether an instance with the given name exists and whether its hash matches. In that case, the artifacts can be re-used.
136+
137+
## The `Specialized` Type Class
138+
139+
The `Specialized` Type Class is erased at runtime. Instances
140+
of `Specialized[T]` are created automatically for types that do not contain type variables.
141+
142+
## A Larger Case Study
143+
144+
As an example of a hierarchy of specialized traits, consider the following small group of specialized collection traits:
145+
146+
```scala
147+
inline trait Iterator[T: Specialized]:
148+
def hasNext: Boolean
149+
def next(): T
150+
151+
inline trait ArrayIterator[T: Specialized](elems: Array[T]) extends Iterator[T]:
152+
private var current = 0
153+
def hasNext: Boolean = current < elems.length
154+
def next(): T = try elems(current) finally current += 1
155+
156+
inline trait Iterable[T: Specialized]:
157+
def iterator: Iterator[T]
158+
def forall(f: T => Unit): Unit =
159+
val it = iterator
160+
while it.hasNext do f(it.next())
161+
162+
inline trait Seq[T: Specialized](elems: Array[T]) extends Iterable[T]:
163+
def length: Int = elems.length
164+
def apply(i: Int): T = elems(i)
165+
def iterator: Iterator[T] = new ArrayIterator[T](elems) {}
166+
```
167+
168+
This generates the following instance traits:
169+
170+
```scala
171+
trait Iterator$sp$Int extends Iterator[Int]:
172+
def hasNext: Boolean
173+
def next(): Int
174+
175+
trait ArrayIterator$sp$Int extends ArrayIterator[Int], Iterator[Int]
176+
177+
trait Iterable$sp$Int extends Iterable[Int]:
178+
def iterator: Iterator$sp$Int
179+
def forall(f: Int => Unit): Unit
180+
181+
trait Seq$sp$Int extends Seq[Int], Iterable[Int]:
182+
def length: Int
183+
def apply(i: Int): Int
184+
```
185+
Note that these traits repeat the parent types of their corresponding inline traits, for instance `ArrayIterator$sp$Int` extends `ArrayIterator[Int]` as well as its parent `Iterator[Int]`. After erasure, the definition of
186+
`ArrayIterator$sp$Int` becomes
187+
```scala
188+
trait ArrayIterator$sp$Int extends ArrayIterator, Iterator$sp$Int
189+
```
190+
Hence, the erased `trait ArrayIterator$sp$Int` extends the general `ArrayIterator` trait as well as the specialized `Iterator$sp$Int` parent trait, which is what we want.
191+
192+
The specialized implementation classes for `ArrayIterator` and `Seq` are as follows:
193+
```scala
194+
class ArrayIterator$impl$Int(elems: Array[Int]) extends ArrayIterator$sp$Int:
195+
private var current = 0
196+
override def hasNext: Boolean =
197+
current < elems.length
198+
override def next(): Int =
199+
try elems(current) finally current += 1
200+
201+
class Seq$impl$Int(elems: Array[Int]) extends Seq$sp$Int:
202+
override def iterator: Iterator$sp$Int = new ArrayIterator$impl$Int(elems)
203+
204+
override def forall(f: Int => Unit): Unit =
205+
val it = iterator
206+
while it.hasNext do f(it.next())
207+
override def length: Int = elems.length
208+
override def apply(i: Int): Int = elems(i)
209+
```
210+
These implementation classes are type correct as long as we inject the knowledge that a specialization trait
211+
like `Seq$sp$Int` is equal to its parameterized version `Seq[Int]`. This equality holds once types are erased.
212+
Before that we either have to assume it, or insert some casts, as shown in the test file
213+
`tests/pos/specialized-traits-strawman.scala`.
214+
215+
After erasure, the implementation traits and classes look like this:
216+
217+
```scala
218+
trait Iterator$sp$Int extends Iterator:
219+
def hasNext: Boolean
220+
def next(): Int
221+
222+
trait ArrayIterator$sp$Int extends ArrayIterator, Iterator$sp$Int
223+
224+
trait Iterable$sp$Int extends Iterable:
225+
def iterator: Iterator$sp$Int
226+
def forall(f: Function1): Unit
227+
228+
trait Seq$sp$Int extends Seq, Iterable$sp$Int:
229+
def length: Int
230+
def apply(i: Int): Int
231+
232+
class ArrayIterator$impl$Int(elems: Int[]) extends ArrayIterator$sp$Int:
233+
private var current = 0
234+
override def hasNext: Boolean =
235+
current < elems.length
236+
override def next(): Int =
237+
try elems(current) finally current += 1
238+
239+
/* Bridges:
240+
override def next(): Object = Int.box(next())
241+
*/
242+
end ArrayIterator$impl$Int
243+
244+
class Seq$impl$Int(elems: Int[]) extends Seq$sp$Int:
245+
override def iterator: Iterator$sp$Int =
246+
new ArrayIterator$impl$Int(elems)
247+
override def forall(f: Function1): Unit =
248+
val it = iterator
249+
while it.hasNext do f.apply$mcVI$sp(it.next())
250+
override def length: Int = elems.length
251+
override def apply(i: Int): Int = elems(i)
252+
253+
/* Bridges:
254+
override def iterator: Iterator = iterator
255+
override def apply(i: Int): Object = Int.box(apply(i))
256+
*/
257+
end Seq$impl$Int
258+
```
259+
Here, `f.apply$mcVI$sp` is the specialized apply method of `Function1` at type `Int => Unit`.
260+
This method is generated by Scala 2's function specialization which is also adopted by Scala 3.
261+
262+
The example shows that indeed all code is properly specialized with no need for box or unbox operations.
263+
264+
265+
## Conclusion
266+
267+
The described scheme is surprisingly simple. All the heavy lifting is done by inline traits. Adding specialization on top requires little more than arranging for a cache of specialized instances.
268+
269+
The scheme requires explicit monomorphization through inline methods and inline traits. One point to investigate further is how convenient and expressive code adhering to that restriction can be. If we take specialized collections as
270+
an example, if we want the result of `map` to be specialized, we have to define `map` as an inline method:
271+
```scala
272+
package collection.immutable.faster
273+
inline trait Vector[+A: Specialized](elems: A*):
274+
...
275+
inline def map[B: Specialized](f: A => B): Vector[B] =
276+
new Vector[B](elems.map(f))
277+
```
278+
There's precedent for this in Kotlin where the majority of higher-order collection methods are declared inline, in this case in order to allow specialization for suspendability. So the restriction does not look like a blocker.
279+
280+
Some flexibility could be gained if we allowed method overloading between specialized inline methods and normal methods with matching type signatures. For instance, the `Vector` implementation above seriously restricts `map` by requiring that its `B` type parameter is also `Specialized`. Thus `map` cannot be used to map a specialized collection to another collection if the result element type is not ground. But we could alleviate the problem by allowing a second, overloaded `map` operation like this:
281+
```scala
282+
def map[B](f: A => B): collection.immutable.Vector[B] =
283+
new collection.immutable.Vector[B](elems.map(f))
284+
```
285+
The second implementation of `map` will return an unspecialized vector if
286+
the new element type is not statically known. If overloads like this were allowed, they could be resolved by picking the specialized inline version if
287+
a `Specialized` instance can be synthesized for the actual type argument, and picking the unspecialized version otherwise.
288+
289+
290+
291+
292+
293+
294+

tests/pos/specialized-traits-strawman.scala

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import language.experimental.erasedDefinitions
1717
val it = iterator
1818
while it.hasNext do f(it.next())
1919

20-
/*inline*/ trait Vec[T/*: Specialized*/](elems: Array[T])
20+
/*inline*/ trait Seq[T/*: Specialized*/](elems: Array[T])
2121
extends Iterable[T]:
2222
def length: Int = elems.length
2323
def apply(i: Int): T = elems(i)
@@ -35,7 +35,7 @@ trait Iterable_Int extends Iterable[Int]:
3535
def iterator: Iterator_Int
3636
def forall(f: Int => Unit): Unit
3737

38-
trait Vec_Int extends Vec[Int], Iterable[Int]:
38+
trait Seq_Int extends Seq[Int], Iterable[Int]:
3939
def length: Int
4040
def apply(i: Int): Int
4141

@@ -47,8 +47,8 @@ class ArrayIterator_Int$impl(elems: Array[Int]) extends ArrayIterator_Int
4747
override def next(): Int =
4848
try elems(current) finally current += 1
4949

50-
class Vec_Int$impl(elems: Array[Int]) extends Vec_Int
51-
, Vec[Int](elems): // snd parent not needed in actual translation
50+
class Seq_Int$impl(elems: Array[Int]) extends Seq_Int
51+
, Seq[Int](elems): // snd parent not needed in actual translation
5252
override def iterator: Iterator_Int =
5353
new ArrayIterator_Int$impl(elems).asInstanceOf
5454
// cast needed since the compiler does not not know that Iterable[Int] = Iterable_Int
@@ -73,7 +73,7 @@ object InlineTraitAPIs:
7373
def iterator: Iterator[T]
7474
def forall(f: T => Unit): Unit
7575

76-
trait Vec[T] extends Iterable[T]:
76+
trait Seq[T] extends Iterable[T]:
7777
def length: Int
7878
def apply(i: Int): T
7979
end InlineTraitAPIs
@@ -95,7 +95,7 @@ object AfterErasure:
9595
def iterator: Iterator
9696
def forall(f: Function1): Unit
9797

98-
trait Vec:
98+
trait Seq:
9999
def length: Int
100100
def apply(i: Int): Any
101101

@@ -109,7 +109,7 @@ object AfterErasure:
109109
def iterator: Iterator_Int
110110
def forall(f: Function1): Unit
111111

112-
trait Vec_Int extends Vec, Iterable_Int:
112+
trait Seq_Int extends Seq, Iterable_Int:
113113
def length: Int
114114
def apply(i: Int): Int
115115

@@ -125,7 +125,7 @@ object AfterErasure:
125125
*/
126126
end ArrayIterator_Int$impl
127127

128-
class Vec_Int$impl(elems: Array[Int]) extends Vec_Int:
128+
class Seq_Int$impl(elems: Array[Int]) extends Seq_Int:
129129
override def iterator: Iterator_Int =
130130
new ArrayIterator_Int$impl(elems)
131131
override def forall(f: Function1): Unit =
@@ -138,5 +138,5 @@ object AfterErasure:
138138
override def iterator: Iterator = iterator
139139
override def apply(i: Int): Any = Int.box(apply(i))
140140
*/
141-
end Vec_Int$impl
141+
end Seq_Int$impl
142142

0 commit comments

Comments
 (0)