Skip to content

Commit cdd1c14

Browse files
authored
Wording suggestions for the weak let proposal
1 parent 5055d9a commit cdd1c14

File tree

1 file changed

+57
-40
lines changed

1 file changed

+57
-40
lines changed

proposals/NNNN-weak-let.md

Lines changed: 57 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,71 +2,87 @@
22

33
* Proposal: [SE-NNNN](NNNN-weak-let.md)
44
* Authors: [Mykola Pokhylets](https://github.com/nickolas-pohilets)
5-
* Review Manager: TBD
6-
* Status: **Awaiting implementation**
5+
* Review Manager: [John McCall](https://github.com/rjmccall)
6+
* Status: **Awaiting review**
77
* Implementation: [swiftlang/swift#80440](https://github.com/swiftlang/swift/pull/80440)
88
* Upcoming Feature Flag: `WeakLet`
9-
* Review: ([discussion](https://forums.swift.org/t/weak-captures-in-sendable-sending-closures/78498))
9+
* Review: ([discussion](https://forums.swift.org/t/weak-captures-in-sendable-sending-closures/78498)) ([pitch](https://forums.swift.org/t/pitch-weak-let/79271))
10+
11+
[SE-0302]: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0302-concurrent-value-and-concurrent-closures.md
1012

1113
## Introduction
1214

13-
Currently Swift requires weak stored variables to be mutable.
14-
This restriction is rather artificial, and causes friction with sendability checking.
15+
Swift provides weak object references using the `weak` modifier on variables and stored properties. Weak references become `nil` when the object is destroyed, causing the value of the variable to seem to change. Swift has therefore always required `weak` references to be declared with the `var` keyword rather than `let`. However, that causes unnecessary friction with [sendability checking][SE-0302]: because weak references must be mutable, classes and closures with such references are unsafe to share between concurrent contexts. This proposal lifts that restriction and allows `weak` to be combined with `let`.
1516

1617
## Motivation
1718

18-
Currently swift classes with weak stored properties cannot be `Sendable`,
19-
because weak properties have to be mutable, and mutable properties are
20-
not allowed in `Sendable` classes.
19+
Currently, Swift classes with weak stored properties cannot be `Sendable`, because weak properties have to be mutable, and mutable properties are not allowed in `Sendable` classes:
2120

22-
Similarly, closures with `weak` captures cannot be `@Sendable`,
23-
because such captures are implicitly made mutable.
21+
```swift
22+
final class C: Sendable {}
2423

25-
Usually developers are not aware of this implicit mutability and have no intention to modify the captured variable.
26-
Implicit mutability of weak captures is inconsistent with `unowned` or default captures.
24+
final class VarUser: Sendable {
25+
weak var ref1: C? // error: stored property 'ref1' of 'Sendable'-conforming class 'VarUser' is mutable
26+
}
27+
```
2728

28-
Wrapping weak reference into a single-field struct, allows stored properties and captures to be immutable.
29+
Similarly, closures with explicit `weak` captures cannot be `@Sendable`, because such captures are implicitly *made* mutable, and `@Sendable` closures cannot capture mutable variables. This is surprising to most programmers, because every other kind of explicit capture is immutable. It is extremely rare for Swift code to directly mutate a `weak` capture.
2930

3031
```swift
31-
final class C: Sendable {}
32+
func makeClosure() -> @Sendable () -> Void {
33+
let c = C()
34+
return { [weak c] in
35+
c?.foo() // error: reference to captured var 'c' in concurrently-executing code
36+
37+
c = nil // allowed, but surprising and very rare
38+
}
39+
}
40+
```
3241

42+
In both cases, allowing the weak reference to be immutable would solve the problem, but this is not currently allowed:
43+
44+
```swift
45+
final class LetUser: Sendable {
46+
weak let ref1: C? // error: 'weak' must be a mutable variable, because it may change at runtime
47+
}
48+
```
49+
50+
The restriction that weak references have to be mutable is based on the idea that the reference is mutated when the referenced object is destroyed. Since it's mutated, it must be kept in mutable storage, and hence the storage must be declared with `var`. This way of thinking about weak references is problematic, however; it does not work very well to explain the behavior of weak references that are components of other values, such as `struct`s. For example, a return value is normally an immutable value, but a `struct` return value can contain a weak reference that may become `nil` at any point.
51+
52+
In fact, wrapping weak references in a single-property `struct` is a viable workaround to the `var` restriction in both properties and captures:
53+
54+
```swift
3355
struct WeakRef {
3456
weak var ref: C?
3557
}
3658

37-
final class User: Sendable {
38-
weak let ref1: C? // error: 'weak' must be a mutable variable, because it may change at runtime
39-
let ref2: WeakRef // ok
59+
final class WeakStructUser: Sendable {
60+
let ref: WeakRef // ok
4061
}
4162

4263
func makeClosure() -> @Sendable () -> Void {
4364
let c = C()
44-
return { [weak c] in
45-
c?.foo() // error: reference to captured var 'c' in concurrently-executing code
46-
c = nil // nobody does this
47-
}
4865
return { [c = WeakRef(ref: c)] in
4966
c.ref?.foo() // ok
5067
}
5168
}
5269
```
5370

54-
Existence of this workaround shows that ban on `weak let` variables is artificial, and can be lifted.
71+
The existence of this simple workaround is itself an argument that the prohibition of `weak let` is not enforcing some fundamentally important rule.
72+
73+
It is true that the value of a `weak` variable can be observed to change when the referenced object is destroyed. However, this does not have to be thought of as a mutation of the variable. A different way of thinking about it is that the variable continues to hold the same weak reference to the object, but that the program is simply not allowed to observe the object through that weak reference after the object is destroyed. This better explains the behavior of weak references in `struct`s: it's not that the destruction of the object changes the `struct` value, it's that the weak reference that's part of the `struct` value will now return `nil` if you try to observe it.
5574

56-
Note that resetting weak references on object destruction is different from regular variable modification.
57-
Resetting on destruction is implemented in a thread-safe manner, and can safely coexist with concurrent reads or writes.
58-
But regular writing to a variable requires exclusive access to that memory location.
75+
Note that all of this relies on the fact that the thread-safety of observing a weak reference is fundamentally different from the thread-safety of assigning `nil` into a `weak var`. Swift's weak references are thread-safe against concurrent destruction: well-ordered reads and writes to a `weak var` or `weak let` will always behave correctly even if the referenced object is concurrently destroyed. But they are not *atomic* in the sense that writing to a `weak var` will behave correctly if another context is concurrently reading or writing to that same `var`. In this sense, a `weak var` is like any other `var`: mutations need to be well-ordered with all other accesses.
5976

6077
## Proposed solution
6178

62-
Allow `weak let` declarations for local variables and stored properties.
79+
`weak` can now be freely combined with `let` in any position that `weak var` would be allowed.
6380

64-
Proposal maintains status quo regarding use of `weak` on function arguments and computed properties:
65-
* there is no valid syntax to indicate that function argument is a weak reference;
66-
* `weak` on computed properties is allowed, but has not effect.
81+
This proposal maintains the status quo regarding `weak` on function arguments and computed properties:
82+
* There is no valid syntax to indicate that function argument is a weak reference.
83+
* `weak` on computed properties is allowed, but has no effect.
6784

68-
Weak captures are immutable under this proposal. If mutable capture is desired,
69-
mutable variable need to be explicit declared and captured.
85+
An explicit `weak` capture is now immutable under this proposal, like any other explicit capture. If the programmer really needs a mutable capture, they must capture a separate `weak var`:
7086

7187
```swift
7288
func makeClosure() -> @Sendable () -> Void {
@@ -76,31 +92,32 @@ func makeClosure() -> @Sendable () -> Void {
7692
c?.foo()
7793
c = nil // error: cannot assign to value: 'c' is an immutable capture
7894
}
95+
}
7996

97+
func makeNonSendableClosure() -> () -> Void {
98+
let c = C()
8099
weak var explicitlyMutable: C? = c
81100
// Closure cannot be @Sendable anymore
82101
return {
83102
explicitlyMutable?.foo()
84-
explicitlyMutable = nil // but assigned is ok
103+
explicitlyMutable = nil // ok
85104
}
86105
}
87106
```
88107

89108
## Source compatibility
90109

91-
Allowing `weak let` bindings is a source-compatible change
92-
that makes previously invalid code valid.
110+
Allowing `weak let` bindings is an additive change that makes previously invalid code valid. It is therefore perfectly source-compatible.
93111

94-
Treating weak captures as immutable is a source-breaking change.
95-
Any code that attempts to write to the capture will stop compiling.
112+
Treating weak captures as immutable is a source-breaking change. Any code that attempts to write to the capture will stop compiling.
96113
The overall amount of such code is expected to be small.
97114

115+
Since the captures of a closure are opaque and cannot be observed outside of the closure, changing the mutability of weak captures has no impact on clients of the closure.
116+
98117
## ABI compatibility
99118

100-
This is an ABI-compatible change.
119+
There is no ABI impact of this change.
101120

102121
## Implications on adoption
103122

104-
This feature can be freely adopted and un-adopted in source
105-
code with no deployment constraints and without affecting source or ABI
106-
compatibility.
123+
This feature can be freely adopted and un-adopted in source code with no deployment constraints and without affecting source or ABI compatibility.

0 commit comments

Comments
 (0)