SE-0507: Borrow and Mutate Accessors
* Proposal: [SE-0507](0507-borrow-accessors.md) * Authors: [Meghana Gupta](https://github.com/meg-gupta), [Tim Kientzle](https://github.com/tbkka) * Review Manager: [Doug Gregor](https://github.com/DougGregor/) * Status: **Implemented (Swift 6.4)** * Vision: [[Prospective Vision] Accessors](https://forums.swift.org/t/prospective-vision-accessors/76707) * Implementation: Available on `main` by default * Review: ([pitch](https://forums.swift.org/t/pitch-borrowing-accessors/83933)) ([review](https://forums.swift.org/t/se-0507-borrow-and-mutate-accessors/84376)) ([acceptance](https://forums.swift.org/t/accepted-se-0507-borrow-and-mutate-accessors/85266))
Introduction
Borrowing accessors — introduced with the new keywords borrow and mutate — allow implementing computed properties and subscripts using borrowing semantics. These augment the existing get, set, yielding borrow, and yielding mutate accessors to complete the design described in the “Prospective Vision for Accessors”: https://forums.swift.org/t/prospective-vision-accessors/76707
To briefly summarize the discussion in that document, borrowing accessors have advantages over other accessor varieties in these circumstances:
- Unlike
getaccessors, borrowing accessors can expose a stored value without copying it - Unlike
yielding borrowandyielding mutateaccessors, borrowing accessors do not require the overhead of a coroutine, making them more performant when they cannot be fully inlined.
Note that borrowing accessors do not replace all uses of the existing accessor variants. In particular, get, yielding mutate, and yielding borrow can all provide access to constructed temporary values. This makes yielding mutate and yielding borrow the most flexible options for general-purpose protocols whose conformers may need to expose temporary values. In contrast, borrowing accessors can only expose values whose storage is guaranteed to be valid until the next mutation of the containing value.
Motivation
The existing accessor variations have limitations that make them unsuitable for certain uses.
A get accessor must either copy an existing value or construct a new value to return to the client. This makes it unsuitable when you have stored data that is expensive or impossible to copy. In particular, collections that store non-copyable values cannot use get for their subscript operations.
struct NC: ~Copyable { ... }
struct ContainerOfNoncopyable {
private var _element: NC
var element: Element {
return _element // 🛑 ERROR: Cannot copy `_element`
}
}The yielding mutate and yielding borrow accessors satisfy a different need. By exposing the access as a coroutine, they allow the provider to run code both before the value is exposed and after the client is done using the value. This supports APIs such as the Dictionary subscript operation which constructs a new value for the duration of the access and destroys it when the access is complete. Yielding accessors are also used to implement resilient access to class properties so that the provider can execute runtime exclusivity checks both before and after the access.
But the coroutines used by yielding accessors have drawbacks: They can add significant overhead to allocate working space for the coroutine and to make multiple function calls into the coroutine. They also limit the access scope. The coroutine and all dependent accesses must complete before the end of the calling function:
struct Element: ~Copyable {
var span: Span<...> { ... }
}
struct Wrapper: ~Copyable {
private var _element: Element
var element: Element {
yielding borrow { // ❗️Note: Using `yielding borrow` accessor
yield _element
}
}
}
func getSpan(wrapper: borrowing Wrapper) -> Span<...> {
// Because we're reading `element` from a yielding accessor,
// its access must finish before `getSpan` returns.
// But `span` cannot outlive `element`, so ...
// 🛑 ERROR: lifetime-dependent value escapes its scope
return wrapper.element.span
}>For more information about yielding mutate/yielding borrow, see: https://github.com/swiftlang/swift-evolution/blob/main/proposals/0474-yielding-accessors.md
Proposed solution
Borrowing accessors are defined similarly to any other accessor, using the context-sensitive borrow and mutate keywords. They use the return keyword to indicate the value being exposed. The mutating version uses & to further indicate that the value is being exposed for potential mutation:
struct RigidWrapper<Element: ~Copyable>: ~Copyable {
var _element: Element
var element: Element {
borrow {
return _element
}
mutate {
return &_element
}
}
}Note: We’ve included an explicit return keyword in the above examples to clarify the distinction with yielding borrow and yielding mutate which use a yield keyword. As with other return values in Swift, the return keyword is optional when the body is a single expression.
Detailed design
The above example shows how `borrow` and `mutate` accessors can be defined. Note that the value being returned must be a stored value that will outlive the execution of the accessor. It is illegal to return a local or temporary value:
```swift
struct InvalidExamples {
var _array : [Int]
var local: [Int] {
borrow {
let foo = [1, 2, 3]
// 🛑 ERROR: Cannot return local value from borrow accessor
return foo
}
}
var temporary: [Int]? {
borrow {
// This would require creating a temporary local
// optional array from `_array`.
// 🛑 ERROR: Cannot return temporary value from borrow accessor
return _array
}
}
}
```
The restrictions on returning temporary values also restricts certain uses of optionals:
```
struct Source {
var _s: String? = ""
var s: String? {
borrow { _s }
}
}
struct Wrapper {
var i: Source?
var prop: String? {
// 🛑 Error: If `i == nil`, this would require a
// temporary `String?.none` to be returned.
borrow { i?.s }
}
}
```
#### Reading from properties that use `borrow`
Clients read a value via a `borrow` accessor using the same code they might use for a property implemented with `get`. However, when a property is implemented with `borrow`, reading the value does not copy. To preserve memory consistency, Swift’s exclusivity rules prevent the provider from being mutated while the borrow is active:
```swift
var owner = Wrapper(value)
// "borrow" the value to give to a function
// without copying...
doSomething(with: owner.element)
func doSomething(with value: borrowing Element) {
// `value` is borrowed, so this invokes
// the method "in-place"
value.someMethod()
// Exclusivity prevents the owner from being
// mutated while `value` is alive:
owner.mutatingMethod() // 🛑 ERROR
}
```
#### Modifying properties that use `mutate`
Using a property implemented with `mutate` is similar. It gives you read/write access for the duration of the borrow:
```swift
var owner = Wrapper(value)
// Mutating/inout access will invoke the `mutate` accessor
doSomething(with: &owner.element)
func doSomeMutation(with value: inout Element) {
// So this invokes a method on the value "in-place"
// Because you borrowed for mutation, this can be
// a mutating method.
value.someMutatingMethod()
// Accessing the owner is an exclusivity violation
owner.anyMethod() // 🛑 ERROR
}
```
#### Compatibility with other accessors
If you provide a `mutate` accessor:
* You must also provide a `borrow` accessor.
* You cannot have a `yielding mutate` or `yielding borrow`
These follow from two considerations:
First, Swift generally disallows write-only properties.
For example, Swift does not allow a property to only include a `set` accessor.
Secondly, we want to ensure compatible access scopes for read and write operations.
This not only ensures consistent behavior to clients of the property,
it also potentially allows the compiler to optimize a mix of read and write operations with a single unified `mutate` access.
If you provide a `borrow` accessor:
* You cannot also define a `get` or `yielding borrow`
This point is partly motivated by the same considerations as above.
In addition, we prohibit multiple read accessors (such as `borrow` and `get`) or
multiple write accessors (such as `mutate` and `yielding mutate`) because it
creates confusion in the caller as to which one will ultimately be used.[^2]
[^2]: In many cases, the compiler will synthesize multiple read or multiple write accessors in order to preserve ABI guarantees over time. In every such case, the ABI considerations dictate which accessors will actually get used in a particular situation.
#### Ownership variations
By default, a `borrow` accessor is considered to not mutate the containing
value,
and a `mutate` accessor is assumed to act as a mutation of the
containing value.
This can be explicitly overridden by using a `mutating` or `nonmutating` modifier,
similarly to `mutating get` or `nonmutating set`.
In these combinations, the `mutating` or `nonmutating` prefix indicates
whether the operation is considered to mutate the containing value.
For example, a `mutating borrow` indicates that even though the
caller can only use this to read the property,
there can be side-effects that alter the containing value in other ways.
As a result, the property can only be accessed in a context
that allows mutation:
```swift
struct S1 {
private var cachedValue: Foo
var foo : Foo {
mutating borrow {
if !cachedValue.available {
// Update `cachedValue`
// Compiler allows such update
// because this is `mutating`
}
return cachedValue
}
}
}
let s1: S1 // Note: Immutable value
s1.foo // 🛑 Cannot use mutating accessor on immutable value
```
Similarly, a `nonmutating mutate` would provide mutable borrow access to some value,
but a mutation of that value is _not_ a mutation of the parent value.
For example, this might be true if the accessor is providing access to a value
that is stored outside of the parent value:
```swift
struct Outer {
var inner: InnerType {
borrow {
return some_value_stored_elsewhere
}
nonmutating mutate {
return &some_value_stored_elsewhere
}
}
}
```
In this example, `inner` can be mutated, but such mutation is not considered to be a mutation of `Outer` for purposes of exclusivity and ownership diagnostics.
#### `borrow` and `mutate` as protocol requirements
Borrowing accessors can also appear as protocol requirements
```swift
protocol BorrowingAccess {
associatedtype Element
var element: Element { borrow mutate }
}
```
Requiring specific accessors in a protocol has two effects:
* It controls how clients can access properties through the protocol. This includes access to properties on an existential or on a protocol-constrained generic argument.
* It requires these accessors to be present on any conforming type, either by being explicitly implemented, or by having the compiler synthesize implementations that call into whatever accessor was implemented.
If the implementation provides a stored property, the compiler can synthesize both `borrow` and `mutate` accessors to satisfy a protocol requirement.
If the implementation provides a `borrow` accessor, the compiler will be able to synthesize either a `yielding borrow` accessor (by yielding the borrowed reference) or a `get` accessor (only for copyable values).
If the implementation provides a `mutate` accessor, the compiler can synthesize either a `set` or `yielding mutate` accessor to satisfy a protocol requirement.
If the protocol requires a `borrow` accessor, the conforming type must provide a `borrow` accessor.
If the protocol requires a `mutate` accessor, the conforming type must provide both a `mutate` accessor and a `borrow` accessor.
No combinations other than those specified above are supported.
#### Cannot use for properties of classes or actors
Classes require runtime exclusivity checks to run both before and after each property access. Since borrowing accessors do not provide a way for the provider to run code after the access, they cannot be used for properties of classes or actors.[^1]
[^1]: `yielding borrow` and `yielding mutate` accessors can be used for properties of classes.
```swift
class ClassType {
private var _value: SomeType
var value: SomeType {
borrow {
// 🛑 Cannot use borrow to implement a property of a class or actor type
return _value
}
mutate {
// 🛑 Cannot use mutate to implement a property of a class or actor type
return &_value
}
}
}
```
#### Use for subscripts
Borrowing accessors can also be used to implement subscript operations:
```swift
struct ArrayLikeType {
subscript(index: Int) -> Element {
borrow { .... }
mutate { .... }
}
}
```
Like any `borrow` or `mutate` operation, the above subscript implicitly accesses the entire struct for the duration of the property access.
In particular, the following is illegal because it creates two
mutating accesses of `x` for the duration of the function call:
```swift
var x: ArrayLikeType
swap(&x[0], &x[1])
```
#### Globals
You may not borrow or mutate a global `var`.
You are allowed to borrow a global `let`, but not mutate it.
```swift
var mutableGlobal: SomeType
let constantGlobal: SomeType
struct BorrowingGlobals {
var mutable: SomeType {
borrow {
// 🛑 Cannot borrow a mutable global
return mutableGlobal
}
mutate {
// 🛑 Cannot mutate a mutable global
return &mutableGlobal
}
}
var constant: SomeType {
borrow {
// OK
return constantGlobal
}
mutate {
// 🛑 Cannot mutate a non-mutable value
return &constantGlobal
}
}
}
```Source compatibility
This could potentially change the interpretation of an existing accessor that uses a function called borrow or mutate that takes a trailing closure parameter:
struct S {
func borrow(closure: () -> ()) { ... }
// Is this a new borrow accessor?
// Or a call to the borrow method just above?
var property: Int { borrow { ... } }
}We believe the above problem is unlikely to arise in practice.
ABI compatibility
This is a new feature that has no impact on existing ABI.
Implications on adoption
Changing an existing non-borrowing accessor to a borrowing accessor or vice-versa is generally ABI-breaking.
However, the ABI of existential types is preserved when concrete conformers change their accessors, as long as the compiler is able to continue synthesizing a conforming accessor.
Changing an existing non-borrowing accessor to a borrowing accessor or vice-versa can be source-breaking for clients. In particular, changing a get to a borrow creates new restrictions on clients regarding the relative lifetimes of different values. This can cause previously working code to no longer compile.
Future directions
Borrowing returns
It is also useful for functions to be able to return borrowed values. As with borrowing accessors, this requires the value to have a guaranteed lifetime. It is particularly useful to extend the borrow semantics:
struct S<Value> {
subscript(_ index: Int) -> Value {
borrow { ... }
}
func indirect(_ parameter: Foo) -> borrowing Value {
let index = ... compute index from parameter ...
return self[index]
}
}Borrowing via unsafe pointers
Low-level data structures are often built using unsafe pointers. Unsafe pointers by their nature prevent the compiler from accurately diagnosing lifetimes, which means that it must generally reject code like the following:
var _storage: UnsafePointer<Element>
var first: Element {
borrow {
// ERROR: borrow accessors can only return stored properties
// or computed properties that have borrow accessors
return _storage.pointee
}
}Supporting cases like this will require some way to annotate the return expression. For example, we might provide a function-like marker:
var first: Element {
borrow {
return unsafeResultDependsOnSelf(_storage.pointee)
}
}This would assert that the result of the expression is valid at least until the next mutating operation on self.
Multiple Returns
The current implementation does not handle borrow or mutate accessors that have more than one return statement.
struct Wrapper {
var selector: Bool
var i: SomeType
var j: SomeType
var prop: SomeType {
borrow {
if selector {
return i
} else {
return j
}
}
}
}Interaction with borrowing switch
The current implementation does not support borrowing switch statements, due to known gaps in the borrowing switch implementation that would need to be resolved first.
struct Box {
enum E {
case a(SomeType)
case b(SomeType)
}
var value: E
var prop: SomeType {
borrow {
switch value {
case a(let va): return va
case b(let vb): return vb
}
}
}
}Local computed properties that provide borrowing of captured values
We do not support using borrow or mutate to define a closure.
func f() {
var storage: [Int]
var property: Span<Int> {
borrow {
storage.span
}
}
}Let properties of classes
In the future, we should be able to support borrow accessors on let properties of classes.
Alternatives considered
Do Nothing
Yielding coroutine-based accessors provide similar functionality, but have distinctly different capabilities and performance characteristics: Coroutine accessors can run code after the end of an access (they are semantically more capable) but that additional capability requires the client to make two function calls unless the accessor can be fully inlined.
The in-place mutation capabilities of borrowing accessors provides much of the same capability without the overhead of multiple function calls.
Acknowledgments
The Swift Standard Library team provided impetus for this feature, especially Karoy Lorentoy, Guillaume Lessard, and Alejandro Alonso. Valuable design advice and insights came from Andrew Trick, Nate Chandler, Joe Groff, and especially John McCall.