Skip to content

Commit 4d812df

Browse files
committed
Update polymorphism section in cc language ref
1 parent c82b623 commit 4d812df

File tree

1 file changed

+133
-45
lines changed

1 file changed

+133
-45
lines changed

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

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

1754
def allListeners: Set[Listener^{X}] = listeners
1855
```
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.
56+
Here, `X^` is a _capture-set variable_. It may appear inside capture sets throughout the class body.
57+
The field listeners holds exactly the listeners that capture X, and register only accepts such
58+
listeners.
2259

2360
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.
2461

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:
62+
#### Under the hood
63+
64+
Capture-set variables without user-provided bounds range over the interval
65+
`>: {} <: {caps.cap}` which is the full lattice of capture sets. They behave like type parameters
66+
whose domain is "all capture sets", not all types.
67+
68+
Under the hood, a capture-set variable is implemented as a normal type parameter with special bounds:
2969
```scala
3070
class Source[X >: CapSet <: CapSet^]:
3171
...
3272
```
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:
73+
`CapSet` is a sealed marker trait in `caps` used internally to distinguish capture-set variables.
74+
It cannot be instantiated or extended; in non–capture-checked code, `CapSet^{a}` and `CapSet^{a,b}`
75+
erase to plain `CapSet`, while with capture checking enabled their capture sets remain distinct.
76+
This representation is an implementation detail and should not be used directly.
77+
78+
#### Instantiation and inference
79+
Capture-set variables are inferred in the same way as ordinary type variables.
80+
They can also be instantiated explicitly:
4381
```scala
4482
class Async extends caps.SharedCapability
4583

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

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

69141
## Capability Members
70142

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_:
143+
Capture parameters can also be introduced as *capability members*, in the same way that type
144+
parameters can be replaced with type members. The earlier example
145+
```scala
146+
class Source[X^]:
147+
private var listeners: Set[Listener^{X}] = Set.empty
148+
```
149+
can be written instead as:
73150
```scala
74151
class Source:
75152
type X^
76153
private var listeners: Set[Listener^{this.X}] = Set.empty
77-
... // as before
154+
155+
def register(l: Listener^{this.X]): Unit =
156+
listeners += l
157+
158+
def allListeners: Set[Listener^{this.X}] = listeners
78159
```
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
160+
A capability member behaves like a path-dependent capture-set variable. It may appear in capture
161+
annotations using paths such as `{this.X}`.
85162

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

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

0 commit comments

Comments
 (0)