Skip to content

Commit 1790303

Browse files
committed
Complete SIP on existential containers
1 parent 1d78df7 commit 1790303

File tree

1 file changed

+95
-40
lines changed

1 file changed

+95
-40
lines changed

content/existential-containers.md

Lines changed: 95 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ given Hexagon is Polygon: ...
4545
```
4646

4747
Defining `Polygon` as a type class rather than an abstract class to be inherited allows us to retroactively state that squares are polygons without modifying the definition of `Square`.
48-
Sticking to subtyping only would require the definition of an inneficient and verbose wrapper class.
48+
Sticking to subtyping would require the definition of an inneficient and verbose wrapper class.
4949

5050
Alas, type classes offer limited support for type erasure–the eliding of some type information at compile-time.
5151
Hence, it is difficult to manipulate heterogeneous collections or write procedures returning arbitrary values known to model a particular concept.
@@ -65,83 +65,138 @@ In other words, it is impossible to call `largest` with an heterogeneous sequenc
6565
## Proposed solution
6666

6767
The problems raised above can be worked around if, instead of using generic parameters with a context bound, we use pairs bundling a value with its conformance witness.
68-
For example, we can rewrite `largest` as follows:
68+
In broad strokes, our solution generalizes the following possible implementation of `largest`:
6969

7070
```scala
71-
def largest(xs: Seq[(Any, PolygonWitness)]): Option[(Any, PolygonWitness)] =
72-
xs.maxByOption((a) => a(1).area(a(0)))
71+
trait AnyPolygon:
72+
type Value
73+
val value: Value
74+
val witness: Polygon { type Self = Value }
75+
76+
def largest(xs: Seq[AnyPolygon]): Option[AnyPolygon] =
77+
xs.maxByOption((a) => a.witness.area(a.value))
7378
```
7479

75-
A pair `(Any, PolygonWitness)` conceptually represents a type-erased polygon.
76-
We call this pair an _existential container_ and the remainder of this SIP explains how to express this idea in a single, type-safe abstraction by leveraging Scala 3 features.
80+
The type `AnyPolygon` conceptually represents a type-erased polygon.
81+
It consists of a pair containing some arbitrary value as well as a witness of that value's type being a polygon.
82+
We call this pair an _existential container_, as a nod to a similar feature in Swift, and the remainder of this SIP explains how to express this idea in a single, type-safe abstraction.
7783

7884
### Specification
7985

80-
As mentioned above, an existential container is merely a pair containing a value and a witness of its conformance to some concept(s).
81-
Expressing such a value in Scala is easy: just write `(Square(1) : Any, summon[Square is Polygon] : Any)`.
82-
This encoding, however, does not allow the selection of any method defined by `Polygon` without an unsafe cast due to the widening applied on the witness.
83-
Fortunately, this issue can be addressed with path dependent types:
86+
Existential containers are encoded as follows:
8487

8588
```scala
89+
import language.experimental.{clauseInterleaving, modularity}
90+
91+
/** A type class. */
92+
trait TypeClass:
93+
type Self
94+
8695
/** A value together with an evidence of its type conforming to some type class. */
87-
trait Container[Concept <: TypeClass]:
96+
sealed trait Containing[Concept <: TypeClass]:
8897
/** The type of the contained value. */
89-
type Value : Concept as witness
98+
type Value: Concept as witness
9099
/** The contained value. */
91100
val value: Value
92101

93-
object Container:
94-
/** Wraps a value of type `V` into a `Container[C]` provided a witness that `V is C`. */
102+
object Containing:
103+
/** A `Containing[C]` whose value is known to have type `V`. */
104+
type Precisely[C <: TypeClass, V] =
105+
Containing[C] { type Value >: V <: V }
106+
/** Wraps a value of type `V` into a `Containing[C]` provided a witness that `V is C`. */
95107
def apply[C <: TypeClass](v: Any)[V >: v.type](using V is C) =
96-
new Container[C]:
97-
type Value >: V <: V
98-
val value: Value = v
108+
new Precisely[C, V] { val value: Value = v }
109+
/** An implicit constructor for `Containing.Precisely[C, V]` from `V`. */
110+
given constructor[C <: TypeClass, V : C]: Conversion[V, Precisely[C, V]] =
111+
apply
99112
```
100113

101-
### Compatibility
114+
Given a type class `C`, an instance `Containing[C]` is an existential container, similar to `AnyPolygon` shown before.
115+
The context bound on the definition of the `Value` member provides a witness of `Value`'s conformance to `C` during implicit resolution when a method of the `value` field is selected.
116+
The companion object of `Containing` provides basic support to create containers ergonomically.
117+
For instance:
102118

103-
A justification of why the proposal will preserve backward binary and TASTy compatibility. Changes are backward binary compatible if the bytecode produced by a newer compiler can link against library bytecode produced by an older compiler. Changes are backward TASTy compatible if the TASTy files produced by older compilers can be read, with equivalent semantics, by the newer compilers.
119+
```scala
120+
def largest(xs: Seq[Containing[Polygon]]): Option[Containing[Polygon]] =
121+
xs.maxByOption(_.value.area)
122+
```
104123

105-
If it doesn't do so "by construction", this section should present the ideas of how this could be fixed (through deserialization-time patches and/or alternative binary encodings). It is OK to say here that you don't know how binary and TASTy compatibility will be affected at the time of submitting the proposal. However, by the time it is accepted, those issues will need to be resolved.
124+
To further improve usability, we propose to let the compiler inject the selection of the `value` field implicitly when a method of `Containing[C]` is selected.
125+
That way, one can simply write `xs.maxByOption(_.area)` in the above example, resulting in quite idiomatic scala.
106126

107-
This section should also argue to what extent backward source compatibility is preserved. In particular, it should show that it doesn't alter the semantics of existing valid programs.
127+
```scala
128+
// Version with subtyping:
129+
trait Polygon:
130+
def area: Double
131+
def largest1(xs: Seq[Polygon]): Option[Polygon] =
132+
xs.maxByOption(_.value.area)
108133

109-
### Feature Interactions
134+
// Version with existential containers:
135+
trait Polygon extends TypeClass:
136+
extension (self: Self) def area: Double
137+
def largest2(xs: Seq[Containing[Polygon]]): Option[Containing[Polygon]] =
138+
xs.maxByOption(_.area)
139+
```
110140

111-
A discussion of how the proposal interacts with other language features. Think about the following questions:
141+
### Compatibility
112142

113-
- When envisioning the application of your proposal, what features come to mind as most likely to interact with it?
114-
- Can you imagine scenarios where such interactions might go wrong?
115-
- How would you solve such negative scenarios? Any limitations/checks/restrictions on syntax/semantics to prevent them from happening? Include such solutions in your proposal.
143+
The change in the syntax does not affect any existing code and therefore this proposal has no impact on source compatibility.
116144

117-
### Other concerns
145+
The semantics of the proposed feature is fully expressible in Scala.
146+
Save for the implicit addition of `.value` on method selection when the receiver is an instance of `Containing[C]`, this proposal requires no change in the language.
147+
As a result, it has no backward binary or TASTy compatibility consequences.
118148

119-
If you think of anything else that is worth discussing about the proposal, this is where it should go. Examples include interoperability concerns, cross-platform concerns, implementation challenges.
149+
### Feature Interactions
120150

121-
### Open questions
151+
The proposed feature is meant to interact with implicit search, as currently implemented by the language.
152+
More specifically, given an existential container `c`, accessing `c.value` _opens_ the existential while retaining its type `c.Value`, effectively keeping an _anchor_ (i.e., the path to the scope of the witness) to the interface of the type class.
122153

123-
If some design aspects are not settled yet, this section can present the open questions, with possible alternatives. By the time the proposal is accepted, all the open questions will have to be resolved.
154+
Since no change in implicit resolution is needed, this proposal cannot create unforeseen negative interactions with existing features.
124155

125-
## Alternatives
156+
### Open questions
126157

127-
This section should present alternative proposals that were considered. It should evaluate the pros and cons of each alternative, and contrast them to the main proposal above.
158+
One problem not addressed by the proposed encoding is the support of multiple type classes to form the interface of a specific container.
159+
For example, one may desire to create a container of values whose types conform to both `Polygon` _and_ `Show`.
160+
We have explored possible encodings for such a feature but decided to remove them from this proposal, as support for multiple type classes can most likely be achieved without any additional language change.
128161

129-
Having alternatives is not a strict requirement for a proposal, but having at least one with carefully exposed pros and cons gives much more weight to the proposal as a whole.
162+
Another open question relates to possible language support for shortening the expression of a container type and/or value.
130163

131164
## Related work
132165

133-
This section should list prior work related to the proposal, notably:
166+
Swift support existential containers.
167+
For instance, `largest` can be written as follows in Swift:
168+
169+
```swift
170+
func largest(_ xs: [any Polygon]) -> (any Polygon)? {
171+
xs.min { (a, b) in a.area < b.area }
172+
}
173+
```
174+
175+
Unlike in this proposal, existential containers in Swift are built-in and have a dedicated syntax (i.e., `any P`).
176+
One advantage of Swift's design is that the type system can treat an existential container as supertype of types conforming to that container's interface.
177+
For example, `any Polygon` is supertype of `Square` (assuming the latter conforms to `Polygon`):
178+
179+
```swift
180+
print(largest([Square(), Hexagon()]))
181+
```
182+
183+
In contrast, to avoid possible undesirable complications, this proposal does not suggest any change to the subtyping relation of Scala.
184+
185+
Rust also supports existential containers in a similar way, writing `dyn P` to denote a container bundling some value of a type conforming to `P`.
186+
Similar to Swift, existential containers in Rust are considered supertypes of the types conforming to their bound.
134187

135-
- A link to the Pre-SIP discussion that led to this proposal,
136-
- Any other previous proposal (accepted or rejected) covering something similar as the current proposal,
137-
- Whether the proposal is similar to something already existing in other languages,
138-
- If there is already a proof-of-concept implementation, a link to it will be welcome here.
188+
189+
A more formal exploration of the state of the art as been documented in a research paper presented prior to this SIP [2].
139190

140191
## FAQ
141192

142-
This section will probably initially be empty. As discussions on the proposal progress, it is likely that some questions will come repeatedly. They should be listed here, with appropriate answers.
193+
#### Is there any significant performance overhead in using existential containers?
194+
195+
On micro benchmarks testing method dispatch specifcally, we have measured that dispatching through existential containers in Scala was about twice as slow as traditional virtual method dispatch, which is explained by the extra pointer indirection introduced by an existential container.
196+
This overhead drops below 10% on larger, more realistic benchmarks [2].
143197

144198
## References
145199

146200
1. Stefan Wehr and Peter Thiemann. 2011. JavaGI: The Interaction of Type Classes with Interfaces and Inheritance. ACM Transactions on Programming Languages and Systems 33, 4 (2011), 12:1–12:83. https://doi.org/10.1145/1985342.1985343
147-
2.
201+
2. Dimi Racordon and Eugene Flesselle and Matt Bovel. 2024. Existential Containers in Scala. ACM SIGPLAN International Conference on Managed Programming Languages and Runtimes, pp. 55-64. https://doi.org/10.1145/3679007.3685056
202+

0 commit comments

Comments
 (0)