You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
+
traitList[+A]:
19
+
// Works for pure functions AND capturing functions!
20
+
defmap[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
+
defmap[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`:
11
47
```scala
12
48
classSource[X^]:
13
49
privatevarlisteners:Set[Listener^{X}] =Set.empty
@@ -16,77 +52,128 @@ class Source[X^]:
16
52
17
53
defallListeners:Set[Listener^{X}] = listeners
18
54
```
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.
22
58
23
59
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.
24
60
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:
29
68
```scala
30
69
classSource[X>:CapSet<:CapSet^]:
31
70
...
32
71
```
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:
43
80
```scala
44
81
classAsyncextends caps.SharedCapability
45
82
46
-
deflistener(async: Async):Listener^{async} =???
83
+
deflistener(a: Async):Listener^{a} =???
47
84
48
-
deftest1(async1: Async, others: List[Async]) =
49
-
valsrc=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:
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.
68
139
69
140
## Capability Members
70
141
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
0 commit comments