Skip to content

Commit a4ef838

Browse files
committed
Update polymorphism section in cc language ref
1 parent 4acec12 commit a4ef838

File tree

1 file changed

+132
-45
lines changed

1 file changed

+132
-45
lines changed

docs/_docs/reference/experimental/capture-checking/polymorphism.md

Lines changed: 132 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,44 @@ nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/capture-che
66

77
## Introduction
88

9-
It is sometimes convenient to write operations that are parameterized with a capture set of capabilities. For instance consider a type of event sources
10-
`Source` on which `Listener`s can be registered. Listeners can hold certain capabilities, which show up as a parameter to `Source`:
9+
Capture checking supports capture-polymorphic programming in two complementary styles:
10+
1. **Implicit** capture polymorphism, which is the default and has minimal syntactic overhead.
11+
2. **Explicit** capture polymorphism, which allows programmers to abstract over capture sets directly through explicit generic parameters.
12+
13+
### Implicit Polymorphism
14+
15+
In many cases, such a higher-order functions, we do not need new syntax to be polymorphic over
16+
capturing types. The classic example is `map` over lists:
17+
```scala
18+
trait List[+A]:
19+
// Works for pure functions AND capturing functions!
20+
def map[B](f: A => B): List[B]
21+
```
22+
Due to the conventions established in previous sections, `f: A => B` translates to `f: A ->{cap} B`
23+
under capture checking which means that the function argument `f` can capture any capability, i.e.,
24+
`map` will have `f`'s effects, if we think of capabilities as the only means to induce side effects,
25+
then _capability polymorphism equals effect polymorphism_. By careful choice of notation and the
26+
[capture tunneling](classes.md#capture-tunneling) mechanism for generic types, we get effect
27+
polymorphism _for free_, and no signature changes are necessary on an eager collection type
28+
such as `List`.
29+
30+
Contrasting this against lazy collections such as `LzyList` from the [previous section](classes.md),
31+
the implicit capability polymorphism induces an additional capture set on the result of `map`:
32+
```scala
33+
extension [A](xs: LzyList[A]^)
34+
def map[B](f: A => B): LzyList[B]^{xs, f}
35+
```
36+
Unlike the eager version which only uses `f` during the computation, the lazy counterpart delays the
37+
computation, so that the original list and the function are captured by the result.
38+
This relationship can be succinctly expressed due to the path-dependent result capture set
39+
`{xs, f}` and would be rather cumbersome to express in more traditional effect-type systems
40+
with explicit generic effect parameters.
41+
42+
### Explicit Polymorphism
43+
44+
In some situations, it is convenient or necessary to parameterize definitions by a capture set.
45+
This allows an API to state precisely which capabilities its clients may use. Consider a `Source`
46+
that stores `Listeners`:
1147
```scala
1248
class Source[X^]:
1349
private var listeners: Set[Listener^{X}] = Set.empty
@@ -16,77 +52,128 @@ class Source[X^]:
1652

1753
def allListeners: Set[Listener^{X}] = listeners
1854
```
19-
The type variable `X^` can be instantiated with a set of capabilities. It can occur in capture sets in its scope. For instance, in the example above
20-
we see a variable `listeners` that has as type a `Set` of `Listeners` capturing `X`. The `register` method takes a listener of this type
21-
and assigns it to the variable.
55+
Here, `X^` is a _capture-set variable_. It may appear inside capture sets throughout the class body.
56+
The field listeners holds exactly the listeners that capture X, and register only accepts such
57+
listeners.
2258

2359
Capture-set variables `X^` without user-annotated bounds by default range over the interval `>: {} <: {caps.cap}` which is the universe of capture sets instead of regular types.
2460

25-
Under the hood, such capture-set variables are represented as regular type variables within the special interval
26-
`>: CapSet <: CapSet^`.
27-
For instance, `Source` from above could be equivalently
28-
defined as follows:
61+
#### Under the hood
62+
63+
Capture-set variables without user-provided bounds range over the interval
64+
`>: {} <: {caps.cap}` which is the full lattice of capture sets. They behave like type parameters
65+
whose domain is "all capture sets", not all types.
66+
67+
Under the hood, a capture-set variable is implemented as a normal type parameter with special bounds:
2968
```scala
3069
class Source[X >: CapSet <: CapSet^]:
3170
...
3271
```
33-
`CapSet` is a sealed trait in the `caps` object. It cannot be instantiated or inherited, so its only
34-
purpose is to identify type variables which are capture sets. In non-capture-checked
35-
usage contexts, the type system will treat `CapSet^{a}` and `CapSet^{a,b}` as the type `CapSet`, whereas
36-
with capture checking enabled, it will take the annotated capture sets into account,
37-
so that `CapSet^{a}` and `CapSet^{a,b}` are distinct.
38-
This representation based on `CapSet` is subject to change and
39-
its direct use is discouraged.
40-
41-
Capture-set variables can be inferred like regular type variables. When they should be instantiated
42-
explicitly one supplies a concrete capture set. For instance:
72+
`CapSet` is a sealed marker trait in `caps` used internally to distinguish capture-set variables.
73+
It cannot be instantiated or extended; in non–capture-checked code, `CapSet^{a}` and `CapSet^{a,b}`
74+
erase to plain `CapSet`, while with capture checking enabled their capture sets remain distinct.
75+
This representation is an implementation detail and should not be used directly.
76+
77+
#### Instantiation and inference
78+
Capture-set variables are inferred in the same way as ordinary type variables.
79+
They can also be instantiated explicitly:
4380
```scala
4481
class Async extends caps.SharedCapability
4582

46-
def listener(async: Async): Listener^{async} = ???
83+
def listener(a: Async): Listener^{a} = ???
4784

48-
def test1(async1: Async, others: List[Async]) =
49-
val src = Source[{async1, others*}]
50-
...
51-
```
52-
Here, `src` is created as a `Source` on which listeners can be registered that refer to the `async` capability or to any of the capabilities in list `others`. So we can continue the example code above as follows:
53-
```scala
85+
def test1[X^](async1: Async, others: List[Async^{X}]) =
86+
val src = Source[{async1, X}]
5487
src.register(listener(async1))
5588
others.map(listener).foreach(src.register)
56-
val ls: Set[Listener^{async, others*}] = src.allListeners
89+
val ls: Set[Listener^{async1, X}] = src.allListeners
90+
```
91+
Here, `src` accepts listeners that may capture either the specific capability `async1` or any element of
92+
others. The resulting `allListeners` method reflects this relationship.
93+
94+
#### Transforming collections
95+
A typical use of explicit capture parameters arises when transforming collections of capturing
96+
values—such as `Future`s. In these cases, the API must guarantee that whatever capabilities are
97+
captured by the elements of the input collection are also captured by the elements of the output.
98+
99+
The following example takes an unordered `Set` of futures and produces a `Stream` that yields their
100+
results in the order in which the futures complete. Using an explicit capture variable `C^`, the
101+
signature expresses that the cumulative capture set of the input futures is preserved in the
102+
resulting stream:
103+
```scala
104+
def collect[T, C^](fs: Set[Future[T]]^{C})(using Async^): Stream[Future[T]^{C}] =
105+
val channel = Channel()
106+
fs.forEach.(_.onComplete(v => channel.send(v)))
107+
Stream.of(channel)
57108
```
58-
A common use-case for explicit capture parameters is describing changes to the captures of mutable fields, such as concatenating
59-
effectful iterators:
109+
110+
#### Tracking the evolution of mutable objects
111+
A common use case for explicit capture parameters is when a mutable object’s reachable capabilities
112+
_grow_ due to mutation. For example, concatenating effectful iterators:
60113
```scala
61114
class ConcatIterator[A, C^](var iterators: mutable.List[IterableOnce[A]^{C}]):
62115
def concat(it: IterableOnce[A]^): ConcatIterator[A, {C, it}]^{this, it} =
63116
iterators ++= it // ^
64117
this // track contents of `it` in the result
65118
```
66-
In such a scenario, we also should ensure that any pre-existing alias of a `ConcatIterator` object should become
67-
inaccessible after invoking its `concat` method. This is achieved with [mutation and separation tracking](separation-checking.md) which are currently in development.
119+
In such cases, the type system must ensure that any existing aliases of the iterator become invalid
120+
after mutation. This is handled by [mutation tracking](mutability.md) and [separation tracking](separation-checking.md), which are currently under development.
121+
122+
## Shall I Be Implicit or Explicit?
123+
124+
Implicit capability polymorphism is intended to cover the most common use cases.
125+
It integrates smoothly with existing functional programming idioms and was expressive enough to
126+
retrofit the Scala standard collections library to capture checking with minimal changes.
127+
128+
Explicit capability polymorphism is introduced only when the capture relationships of an API must be
129+
stated directly in its signature. At this point, we have seen several examples where doing so improves
130+
clarity: naming a capture set explicitly, preserving the captures of a collection, or describing how
131+
mutation changes the captures of an object.
132+
133+
The drawback of explicit polymorphism is additional syntactic overhead. Capture parameters can make
134+
signatures more verbose, especially in APIs that combine several related capture sets.
135+
136+
**Recommendation:** Prefer implicit polymorphism by default.
137+
Introduce explicit capture parameters only when the intended capture relationships cannot be expressed
138+
implicitly or would otherwise be unclear.
68139

69140
## Capability Members
70141

71-
Just as parametrization by types can be equally expressed with type members, we could
72-
also define the `Source[X^]` class above using a _capability member_:
142+
Capture parameters can also be introduced as *capability members*, in the same way that type
143+
parameters can be replaced with type members. The earlier example
144+
```scala
145+
class Source[X^]:
146+
private var listeners: Set[Listener^{X}] = Set.empty
147+
```
148+
can be written instead as:
73149
```scala
74150
class Source:
75151
type X^
76152
private var listeners: Set[Listener^{this.X}] = Set.empty
77-
... // as before
153+
154+
def register(l: Listener^{this.X]): Unit =
155+
listeners += l
156+
157+
def allListeners: Set[Listener^{this.X}] = listeners
78158
```
79-
Here, we can refer to capability members using paths in capture sets (such as `{this.X}`). Similarly to type members,
80-
capability members can be upper- and lower-bounded with capture sets:
81-
```scala
82-
trait Thread:
83-
type Cap^
84-
def run(block: () ->{this.Cap} -> Unit): Unit
159+
A capability member behaves like a path-dependent capture-set variable. It may appear in capture
160+
annotations using paths such as `{this.X}`.
85161

86-
trait GPUThread extends Thread:
87-
type Cap^ >: {cudaMalloc, cudaFree} <: {caps.cap}
162+
Capability members can also have capture-set bounds, restricting which capabilities they may contain:
163+
```scala
164+
trait Reactor:
165+
type Cap^ <: {caps.cap}
166+
def onEvent(h: Event ->{this.Cap} Unit): Unit
167+
```
168+
Each implementation of Reactor may refine `Cap^` to a more specific capture set:
169+
```scala
170+
trait GUIReactor extends Reactor:
171+
type Cap^ <: {ui, log}
88172
```
89-
Since `caps.cap` is the top element for subcapturing, we could have also left out the
90-
upper bound: `type Cap^ >: {cudaMalloc, cudaFree}`.
173+
Here, `GUIReactor` specifies that event handlers may capture only `ui`, `log`, or a subset thereof.
174+
The `onEvent` method expresses this via the path-dependent capture set `{this.Cap}`.
175+
176+
Capability members are useful when capture information should be tied to object identity or form part
177+
of an abstract interface, instead of being expressed through explicit capture parameters.
91178

92-
**Advanced uses:** We discuss more advanced uses cases for capability members [here](advanced.md).
179+
**Advanced uses:** We discuss more advanced use cases for capability members [here](advanced.md).

0 commit comments

Comments
 (0)