Skip to content

Commit 1d78df7

Browse files
committed
Start SIP on existential containers
1 parent e0ead64 commit 1d78df7

File tree

1 file changed

+147
-0
lines changed

1 file changed

+147
-0
lines changed

content/existential-containers.md

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
---
2+
layout: sip
3+
permalink: /sips/:title.html
4+
stage: design
5+
status: submitted
6+
presip-thread: n/a
7+
title: SIP-NN - Existential Containers
8+
---
9+
10+
**By: Dimi Racordon and Eugene Flesselle and Matt Bovel**
11+
12+
## History
13+
14+
| Date | Version |
15+
|---------------|--------------------|
16+
| Nov 25th 2024 | Initial Draft |
17+
18+
## Summary
19+
20+
Type classes have become a well-established feature in the Scala ecosystem to escape some of the shortcomings of subtyping with respect to extensibility.
21+
Unfortunately, type classes do not support run-time polymorphism and dynamic dispatch, two features typically taken for granted in Scala.
22+
23+
This SIP proposes a feature called *existential containers* to address this problem.
24+
An existential container wraps a value together with a witness of its conformance to one or several type classes into an object exposing the API defined by these type classes.
25+
26+
## Motivation
27+
28+
Type classes can address some of the well-known limitations of subtyping with respect to extensibility, such as the ability to extend existing data types with new behaviors [1].
29+
A type class describes the interface of a generic _concept_ as a set of requirements, expressed in the form of operations and associated types.
30+
These requirements can be implemented for a specific type, thereby specifying how this type _models_ the concept.
31+
The following illustrates:
32+
33+
```scala
34+
import shapes.{Square, Hexagon}
35+
36+
trait TypeClass:
37+
type Self
38+
39+
trait Polygon extends TypeClass:
40+
extension (self: Self)
41+
def area: Double
42+
43+
given Square is Polygon: ...
44+
given Hexagon is Polygon: ...
45+
```
46+
47+
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.
49+
50+
Alas, type classes offer limited support for type erasure–the eliding of some type information at compile-time.
51+
Hence, it is difficult to manipulate heterogeneous collections or write procedures returning arbitrary values known to model a particular concept.
52+
The following illustrates:
53+
54+
```scala
55+
def largest[T: Polygon](xs: Seq[T]): Option[T] =
56+
xs.maxByOption(_.area)
57+
58+
largest(List(Square(), Hexagon()))
59+
// error: No given instance of type Polygon{type Self = Square | Hex} was found for a context parameter of method largest
60+
```
61+
62+
The call to `largest` is illegal because, although there exist witnesses of the `Polygon` and `Hexagon`'s conformance to `Polygon`, no such witness exists for their least common supertype.
63+
In other words, it is impossible to call `largest` with an heterogeneous sequence of polygons.
64+
65+
## Proposed solution
66+
67+
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:
69+
70+
```scala
71+
def largest(xs: Seq[(Any, PolygonWitness)]): Option[(Any, PolygonWitness)] =
72+
xs.maxByOption((a) => a(1).area(a(0)))
73+
```
74+
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.
77+
78+
### Specification
79+
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:
84+
85+
```scala
86+
/** A value together with an evidence of its type conforming to some type class. */
87+
trait Container[Concept <: TypeClass]:
88+
/** The type of the contained value. */
89+
type Value : Concept as witness
90+
/** The contained value. */
91+
val value: Value
92+
93+
object Container:
94+
/** Wraps a value of type `V` into a `Container[C]` provided a witness that `V is C`. */
95+
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
99+
```
100+
101+
### Compatibility
102+
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.
104+
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.
106+
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.
108+
109+
### Feature Interactions
110+
111+
A discussion of how the proposal interacts with other language features. Think about the following questions:
112+
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.
116+
117+
### Other concerns
118+
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.
120+
121+
### Open questions
122+
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.
124+
125+
## Alternatives
126+
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.
128+
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.
130+
131+
## Related work
132+
133+
This section should list prior work related to the proposal, notably:
134+
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.
139+
140+
## FAQ
141+
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.
143+
144+
## References
145+
146+
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.

0 commit comments

Comments
 (0)