Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 137 additions & 45 deletions docs/_docs/reference/experimental/capture-checking/polymorphism.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,48 @@ nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/capture-che

## Introduction

It is sometimes convenient to write operations that are parameterized with a capture set of capabilities. For instance consider a type of event sources
`Source` on which `Listener`s can be registered. Listeners can hold certain capabilities, which show up as a parameter to `Source`:
Capture checking supports capture-polymorphic programming in two complementary styles:

1. **Implicit** capture polymorphism, which is the default and has minimal syntactic overhead.
1. **Explicit** capture polymorphism, which allows programmers to abstract over capture sets directly through explicit generic parameters.

The difference between implicit and explicit capture polymorphism is analogous to the difference
between polymorphism through subtyping versus parametric polymorphism through type parameters/generics.

### Implicit Polymorphism

In many cases, such a higher-order functions, we do not need new syntax to be polymorphic over
capturing types. The classic example is `map` over lists:
```scala
trait List[+A]:
// Works for pure functions AND capturing functions!
def map[B](f: A => B): List[B]
```
Due to the conventions established in previous sections, `f: A => B` translates to `f: A ->{cap} B`
under capture checking which means that the function argument `f` can capture any capability, i.e.,
`map` will have `f`'s effects, if we think of capabilities as the only means to induce side effects,
then _capability polymorphism equals effect polymorphism_. By careful choice of notation and the
[capture tunneling](classes.md#capture-tunneling) mechanism for generic types, we get effect
polymorphism _for free_, and no signature changes are necessary on an eager collection type
such as `List`.

Contrasting this against lazy collections such as `LzyList` from the [previous section](classes.md),
the implicit capability polymorphism induces an additional capture set on the result of `map`:
```scala
extension [A](xs: LzyList[A]^)
def map[B](f: A => B): LzyList[B]^{xs, f}
```
Unlike the eager version which only uses `f` during the computation, the lazy counterpart delays the
computation, so that the original list and the function are captured by the result.
This relationship can be succinctly expressed due to the path-dependent result capture set
`{xs, f}` and would be rather cumbersome to express in more traditional effect-type systems
with explicit generic effect parameters.

### Explicit Polymorphism

In some situations, it is convenient or necessary to parameterize definitions by a capture set.
This allows an API to state precisely which capabilities its clients may use. Consider a `Source`
that stores `Listeners`:
```scala
class Source[X^]:
private var listeners: Set[Listener^{X}] = Set.empty
Expand All @@ -16,77 +56,129 @@ class Source[X^]:

def allListeners: Set[Listener^{X}] = listeners
```
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
we see a variable `listeners` that has as type a `Set` of `Listeners` capturing `X`. The `register` method takes a listener of this type
and assigns it to the variable.
Here, `X^` is a _capture-set variable_. It may appear inside capture sets throughout the class body.
The field listeners holds exactly the listeners that capture X, and register only accepts such
listeners.

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.

Under the hood, such capture-set variables are represented as regular type variables within the special interval
`>: CapSet <: CapSet^`.
For instance, `Source` from above could be equivalently
defined as follows:
#### Under the hood

Capture-set variables without user-provided bounds range over the interval
`>: {} <: {caps.cap}` which is the full lattice of capture sets. They behave like type parameters
whose domain is "all capture sets", not all types.

Under the hood, a capture-set variable is implemented as a normal type parameter with special bounds:
```scala
class Source[X >: CapSet <: CapSet^]:
...
```
`CapSet` is a sealed trait in the `caps` object. It cannot be instantiated or inherited, so its only
purpose is to identify type variables which are capture sets. In non-capture-checked
usage contexts, the type system will treat `CapSet^{a}` and `CapSet^{a,b}` as the type `CapSet`, whereas
with capture checking enabled, it will take the annotated capture sets into account,
so that `CapSet^{a}` and `CapSet^{a,b}` are distinct.
This representation based on `CapSet` is subject to change and
its direct use is discouraged.

Capture-set variables can be inferred like regular type variables. When they should be instantiated
explicitly one supplies a concrete capture set. For instance:
`CapSet` is a sealed marker trait in `caps` used internally to distinguish capture-set variables.
It cannot be instantiated or extended; in non–capture-checked code, `CapSet^{a}` and `CapSet^{a,b}`
erase to plain `CapSet`, while with capture checking enabled their capture sets remain distinct.
This representation is an implementation detail and should not be used directly.

#### Instantiation and inference
Capture-set variables are inferred in the same way as ordinary type variables.
They can also be instantiated explicitly with capture-set literals or other
capture-set variables:
```scala
class Async extends caps.SharedCapability

def listener(async: Async): Listener^{async} = ???
def listener(a: Async): Listener^{a} = ???

def test1(async1: Async, others: List[Async]) =
val src = Source[{async1, others*}]
...
```
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:
```scala
def test1[X^](async1: Async, others: List[Async^{X}]) =
val src = Source[{async1, X}]
src.register(listener(async1))
others.map(listener).foreach(src.register)
val ls: Set[Listener^{async, others*}] = src.allListeners
val ls: Set[Listener^{async1, X}] = src.allListeners
```
Here, `src` accepts listeners that may capture either the specific capability `async1` or any element of
others. The resulting `allListeners` method reflects this relationship.

#### Transforming collections
A typical use of explicit capture parameters arises when transforming collections of capturing
values, such as `Future`s. In these cases, the API must guarantee that whatever capabilities are
captured by the elements of the input collection are also captured by the elements of the output.

The following example takes an unordered `Set` of futures and produces a `Stream` that yields their
results in the order in which the futures complete. Using an explicit capture variable `C^`, the
signature expresses that the cumulative capture set of the input futures is preserved in the
resulting stream:
```scala
def collect[T, C^](fs: Set[Future[T]^{C}])(using Async^): Stream[Future[T]^{C}] =
val channel = Channel()
fs.forEach.(_.onComplete(v => channel.send(v)))
Stream.of(channel)
```
A common use-case for explicit capture parameters is describing changes to the captures of mutable fields, such as concatenating
effectful iterators:

#### Tracking the evolution of mutable objects
A common use case for explicit capture parameters is when a mutable object’s reachable capabilities
_grow_ due to mutation. For example, concatenating effectful iterators:
```scala
class ConcatIterator[A, C^](var iterators: mutable.List[IterableOnce[A]^{C}]):
def concat(it: IterableOnce[A]^): ConcatIterator[A, {C, it}]^{this, it} =
iterators ++= it // ^
this // track contents of `it` in the result
```
In such a scenario, we also should ensure that any pre-existing alias of a `ConcatIterator` object should become
inaccessible after invoking its `concat` method. This is achieved with [mutation and separation tracking](separation-checking.md) which are currently in development.
In such cases, the type system must ensure that any existing aliases of the iterator become invalid
after mutation. This is handled by [mutation tracking](mutability.md) and [separation tracking](separation-checking.md), which are currently under development.
Comment on lines +115 to +125
Copy link
Contributor

@natsukagami natsukagami Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a poor example, as it instantly leads the reader into writing this (very buggy, doesn't guarantee anything until separation checking) code.

The problem with mutable collections with non-type-parameter element types is that you cannot use implicit capture polymorphism: they get instantiated with the elements at the construction of the collection.

class IteratorList(private var iterators: mutable.List[Iterator[A]^]):
  //                                            ^ this cap will be instantiated with whatever is in the initial list
  // ...
  def +=(it: Iterator[A]^) = iterators += it
  
val xs = IteratorList(mutable.List.empty) // forcefully instantiated to {}

(also we don't have a name to refer to the capture set of the elements, when we write the += method)

Therefore, we need a capture set variable, to allow the user/inference to specify a capture set suitable for the whole usage of the collection.

class IteratorList[C^](private var iterators: mutable.List[Iterator[A]^{C}]):
  // ...
  def +=(it: Iterator[A]^{C}) = iterators += it
  
val xs = IteratorList(mutable.List.empty)
xs += Iterator(async)
xs += Iterator(io)
// inference will (probably) find out that xs: IteratorList[{async, io}] 

Note that this capture set will not change, it's part of the type: it means you have to be able to name all the captures of all the elements at the point of creating the collection. If you want a growing capture set, it's not sound until separation checking.


## Shall I Be Implicit or Explicit?

Implicit capability polymorphism is intended to cover the most common use cases.
It integrates smoothly with existing functional programming idioms and was expressive enough to
retrofit the Scala standard collections library to capture checking with minimal changes.

Explicit capability polymorphism is introduced only when the capture relationships of an API must be
stated directly in its signature. At this point, we have seen several examples where doing so improves
clarity: naming a capture set explicitly, preserving the captures of a collection, or describing how
mutation changes the captures of an object.

The drawback of explicit polymorphism is additional syntactic overhead. Capture parameters can make
signatures more verbose, especially in APIs that combine several related capture sets.

**Recommendation:** Prefer implicit polymorphism by default.
Introduce explicit capture parameters only when the intended capture relationships cannot be expressed
implicitly or would otherwise be unclear.

## Capability Members

Just as parametrization by types can be equally expressed with type members, we could
also define the `Source[X^]` class above using a _capability member_:
Capture parameters can also be introduced as *capability members*, in the same way that type
parameters can be replaced with type members. The earlier example
```scala
class Source[X^]:
private var listeners: Set[Listener^{X}] = Set.empty
```
can be written instead as:
```scala
class Source:
type X^
private var listeners: Set[Listener^{this.X}] = Set.empty
... // as before

def register(l: Listener^{this.X]): Unit =
listeners += l

def allListeners: Set[Listener^{this.X}] = listeners
```
Here, we can refer to capability members using paths in capture sets (such as `{this.X}`). Similarly to type members,
capability members can be upper- and lower-bounded with capture sets:
```scala
trait Thread:
type Cap^
def run(block: () ->{this.Cap} -> Unit): Unit
A capability member behaves like a path-dependent capture-set variable. It may appear in capture
annotations using paths such as `{this.X}`.

trait GPUThread extends Thread:
type Cap^ >: {cudaMalloc, cudaFree} <: {caps.cap}
Capability members can also have capture-set bounds, restricting which capabilities they may contain:
```scala
trait Reactor:
type Cap^ <: {caps.cap}
def onEvent(h: Event ->{this.Cap} Unit): Unit
```
Each implementation of Reactor may refine `Cap^` to a more specific capture set:
```scala
trait GUIReactor extends Reactor:
type Cap^ <: {ui, log}
```
Since `caps.cap` is the top element for subcapturing, we could have also left out the
upper bound: `type Cap^ >: {cudaMalloc, cudaFree}`.
Here, `GUIReactor` specifies that event handlers may capture only `ui`, `log`, or a subset thereof.
The `onEvent` method expresses this via the path-dependent capture set `{this.Cap}`.

Capability members are useful when capture information should be tied to object identity or form part
of an abstract interface, instead of being expressed through explicit capture parameters.

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