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
Copy file name to clipboardExpand all lines: docs/_docs/reference/experimental/capture-checking/basics.md
+57Lines changed: 57 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -172,6 +172,63 @@ def f(x: ->{c} Int): Int
172
172
```
173
173
Here, the actual argument to `f` is allowed to use the `c` capability but no others.
174
174
175
+
## Lazy Vals
176
+
177
+
Lazy vals receive special treatment under capture checking, similar to parameterless methods. A lazy val has two distinct capture sets:
178
+
179
+
1.**The initializer's capture set**: What capabilities the initialization code uses
180
+
2.**The result's capture set**: What capabilities the lazy val's value captures
181
+
182
+
### Initializer Captures
183
+
184
+
When a lazy val is declared, its initializer is checked in its own environment (like a method body). The initializer can capture capabilities, and these are tracked separately:
185
+
186
+
```scala
187
+
defexample(console: Console^) =
188
+
lazyvalx: () ->String=
189
+
console.println("Computing x") // console captured by initializer
190
+
() =>"Hello, World!"// result doesn't capture console
Here, the initializer of `x` uses `console` (to print a message), so accessing `x` for the first time will use the `console` capability. However, the **result** of `x` is a pure function `() -> String` that doesn't capture any capabilities.
197
+
198
+
The type system tracks that accessing `x` requires the `console` capability, even though the resulting value doesn't. This is reflected in the function types: `fun` must declare `{console}` in its capture set because it accesses `x`.
199
+
200
+
### Lazy Val Member Selection
201
+
202
+
When accessing a lazy val member through a qualifier, the qualifier is charged to the current capture set, just like calling a parameterless method:
203
+
204
+
```scala
205
+
traitContainer:
206
+
lazyvallazyMember:String
207
+
208
+
defclient(c: Container^):Unit=
209
+
valf1: () ->String= () => c.lazyMember // error
210
+
valf2: () ->{c} String= () => c.lazyMember // ok
211
+
```
212
+
213
+
Accessing `c.lazyMember` can trigger initialization, which may use capabilities from `c`. Therefore, the capture set must include `c`.
214
+
215
+
### Equivalence with Methods
216
+
217
+
For capture checking purposes, lazy vals behave identically to parameterless methods:
218
+
219
+
```scala
220
+
traitT:
221
+
defmethodMember():String
222
+
lazyvallazyMember:String
223
+
224
+
deftest(t: T^):Unit=
225
+
// Both require {t} in the capture set
226
+
valm: () ->{t} String= () => t.methodMember()
227
+
vall: () ->{t} String= () => t.lazyMember
228
+
```
229
+
230
+
This equivalence reflects that both can trigger computation using capabilities from their enclosing object.
231
+
175
232
## Subtyping and Subcapturing
176
233
177
234
Capturing influences subtyping. As usual we write `T₁ <: T₂` to express that the type
Copy file name to clipboardExpand all lines: docs/_docs/reference/experimental/capture-checking/mutability.md
+61Lines changed: 61 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -215,6 +215,67 @@ val b1: Ref^{a.rd} = a
215
215
valb2:Ref^{cap.rd} = a
216
216
```
217
217
218
+
## Lazy Vals and Read-Only Restrictions
219
+
220
+
Lazy val initializers are subject to read-only restrictions similar to those for normal methods in `Mutable` classes. Specifically, a lazy val initializer cannot call update methods on non-local exclusive capabilities—capabilities defined outside the lazy val's scope.
221
+
222
+
### Read-Only Initialization
223
+
224
+
When a lazy val is declared, its initializer may use capabilities from the surrounding environment. However, it can only access non-local exclusive capabilities in a read-only fashion:
225
+
226
+
```scala
227
+
defexample(r: Ref^) =
228
+
lazyvalbadInit: () ->{r.rd} Int=
229
+
r.set(100) // error: cannot call update method
230
+
() => r.get()
231
+
232
+
lazyvalgoodInit: () ->{r.rd} Int=
233
+
vali= r.get() // ok: read-only access
234
+
() => r.get() + i
235
+
```
236
+
237
+
The initializer of `badInit` attempts to call `r.set(100)`, an update method on the non-local exclusive capability `r`. This is rejected because initializers should not perform mutations on external state.
238
+
239
+
### Local Capabilities
240
+
241
+
The restriction applies only to **non-local** capabilities. A lazy val can freely call update methods on capabilities it creates locally within its initializer:
242
+
243
+
```scala
244
+
defexample=
245
+
lazyvallocalMutation: () =>Int=
246
+
vallocal:Ref^=Ref(10) // created in initializer
247
+
local.set(100) // ok: local capability
248
+
() => local.get()
249
+
```
250
+
251
+
Here, `local` is created within the lazy val's initializer, so it counts as a local capability. The initializer can call update methods on it.
252
+
253
+
### Closures vs. Initializer Code
254
+
255
+
The read-only restriction applies to code that runs during initialization, not to closures created by the initializer:
256
+
257
+
```scala
258
+
defexample(r: Ref^) =
259
+
lazyvalclosure: () ->{r} Int=
260
+
vali= r.get()
261
+
() => r.set(i); r.get() // ok: closure runs later, not during init
262
+
```
263
+
264
+
Closures (like `() => ...`) run at a later time than the initializer, so their update method calls are permitted.
265
+
266
+
### Rationale
267
+
268
+
Since lazy val initialization happens unpredictably (on first access), allowing initializers to mutate non-local state could cause subtle bugs. The read-only restriction ensures:
269
+
270
+
1.**Predictable initialization**: Accessing a lazy val for the first time doesn't have surprising side effects on external mutable state
271
+
2.**Thread safety**: Lazy vals can be safely used in concurrent contexts without worrying about initialization side effects
272
+
3.**Referential transparency**: The timing of first access doesn't affect program correctness
273
+
274
+
This makes lazy vals behave like normal methods in `Mutable` classes: they can read from their environment but cannot update it unless explicitly marked.
275
+
There's currently no `update` modifier for lazy vals, so their initialization is always read-only with respect to non-local capabilities. A future version of capture checking might
276
+
support `update lazy val` if there are compelling use cases and there is sufficient
277
+
community demand.
278
+
218
279
## Update Restrictions
219
280
220
281
If a capability `r` is a read-only access, then one cannot use `r` to call an update method of `r` or to assign to a field of `r`. E.g. `r.set(22)` and `r.current = 22` are both disallowed.
0 commit comments