- Proposal: SE-0310
- Author: Kavon Farvardin
- Review Manager: Doug Gregor
- Status: Implemented (Swift 5.5)
- Decision Notes: Pitch, Acceptance
- Implementation: apple/swift#36430, apple/swift#36670, apple/swift#37225
- Available in recent
main
snapshots.
Nominal types such as classes, structs, and enums in Swift support computed properties and subscripts, which are members of the type that invoke programmer-specified computations when getting or setting them. The recently accepted proposal SE-0296 introduced asynchronous functions via async
, in conjunction with await
, but did not specify that computed properties or subscripts can support effects like asynchrony. Furthermore, to take full advantage of async
properties, the ability to specify that a property throws
is also important. This document aims to partially fill in this gap by proposing a syntax and semantics for effectful read-only computed properties and subscripts.
A read-only computed property is a computed property that only defines a get
accessor. Similarly, a read-only subscript is a subscript that only defines a get
accessor. Throughout the remainder of this proposal, any unqualified mention of a "property" or "subscript" refers to a read-only version of that member. Furthermore, unless otherwise specified, the concepts of synchrony, asynchrony, and the definition of something being "async" or "sync" are as described in SE-0296.
An effect is an observable behavior of a function. Swift's type system tracks a few kinds of effects: throws
indicates that the function may return along an exceptional failure path with an Error
, rethrows
indicates that a throwing closure passed into the function may be invoked, and async
indicates that the function may reach a suspension point.
This proposal's examples use features from a number of other recent proposals, such as structured concurrency and actors. Overviews of those features are out of the scope of this proposal, but basic understanding of the importance of those features is required to fully grasp the motivation of this proposal.
An asynchronous function is designed for computations that may or always will suspend to perform a context switch before returning. Of primary concern in this proposal are scenarios where the use of Swift concurrency features are limited due to the lack of effectful read-only computed properties and subscripts (which will be referred to as simply "effectful properties" from now on), so we will consider those first. Then, we will consider programming patterns in existing Swift code where the availability of effectful properties would help simplify the code.
An asynchronous call cannot appear within a synchronous context. This fundamental restriction means that computed properties and subscripts would be severely limited in their ability to use Swift's new concurrency features. The only concurrency capability available to them is creating detached tasks, but the completion of those tasks cannot be awaited in synchronous contexts in order to produce an answer:
// ...
class Socket {
// ...
public var alive: Bool {
get {
let handle = detach { await self.checkSocketStatus() }
return await handle.get()
// ^~~~~ error: cannot 'await' in a sync context
}
}
private func checkSocketStatus() async -> Bool { /* ... */ }
}
It would be better if the property could announce that it may require a suspension to retrieve an answer by allowing it to be marked as async
. This way, alive
could directly await
the result of checkSocketStatus
.
As one might imagine, a type that would like to take advantage of actors to isolate concurrent access to resources, while exposing information about those resources through properties, is not possible because one must use await
to interact with the actor from outside of its isolation context:
struct Transaction { /* ... */ }
enum BankError: Error { /* ... */}
actor AccountManager {
// NOTE: `getLastTransaction` is viewed as async
// when called from outside of the actor
func getLastTransaction() -> Transaction { /* ... */ }
func getTransactions(onDay: Date) async -> [Transaction] { /* ... */ }
}
class BankAccount {
// ...
private let manager: AccountManager?
var lastTransaction: Transaction {
get {
guard let manager = manager else {
throw BankError.NoManager
// ^~~~~ error: cannot 'throw' in a non-throwing context
}
return await manager.getLastTransaction()
// ^~~~~ error: cannot 'await' in a sync context
}
}
subscript(_ d: Date) -> [Transaction] {
return await manager?.getTransactions(onDay: d) ?? []
// ^~~~~ error: cannot 'await' in a sync context
}
}
The use of throw
in lastTransaction
highlights a design pattern for properties and subscripts that is not available in Swift. Currently, lastTransaction
would need to return values of type Optional<Transaction>
, or some structurally similar enum
or tuple, to account for the possibility of signaling failure. With the ability to throw
, the property could describe what went wrong to its users, as opposed to simply returning nil
, in a form compatible with the established error handling mechanisms in Swift.
Furthermore, a computed property getter cannot accept any explicit arguments, such as a completion handler, because the syntax for accessing a property is fundamentally designed not to accept such arguments. Such restrictions around input arguments are one of the key differences between computed properties and methods. But, with the advent of async
functions, an explicit completion-handler argument is no longer required for the function to be asynchronous. Thus, having async
computed properties does not go against the existing syntax for computed property accesses: it's mainly a distinction in the type system.
According to the API design guidelines, computed properties that do not quickly return, which includes asynchronous operations, are not what programmers typically expect:
Document the complexity of any computed property that is not O(1). People often assume that property access involves no significant computation, because they have stored properties as a mental model. Be sure to alert them when that assumption may be violated.
but, computed properties that may block or fail do appear in practice (see the motivation in this pitch).
As a real-world example of the need for effectful properties, the SDK defines a protocol AVAsynchronousKeyValueLoading
, which is solely dedicated to querying the status of a type's property, while offering an asynchronous mechanism to load the properties. The types that conform to this protocol include AVAsset, which relies on this protocol because its read-only properties are blocking and failable.
Let's distill the problem solved by AVAsynchronousKeyValueLoading
into a simple example. In existing code, it is impossible for property get
access to also accept a completion handler, i.e., a closure for the property to invoke with the result of the operation. Thus, existing code that wished to use computed properties in scenarios where the computation may be blocking must use various workarounds. One workaround is to define an additional asynchronous version of the property as a method that accepts a completion handler:
class NetworkResource {
var isAvailable: Bool {
get { /* a possibly blocking operation */ }
}
func isAvailableAsync(completionHandler: ((Bool) -> Void)?) {
// method that returns without blocking.
// completionHandler is invoked once operation completes.
}
}
The problem with this code is that, even with a comment on isAvailable
to document that a get
on this property may block, the programmer may mistakenly use it instead of isAvailableAsync
because it is easy to ignore a comment. But, if isAvailable
's get
were marked with async
, then the type system will force the programmer to use await
, which tells the programmer that the property's access may suspend until the operation completes. Thus, this async
effect specifier enhances the recommendation made in the API design guidelines by leveraging the type checker to warn users that the property access may involve significant computation.
For the problems detailed in the motivation section, the proposed solution is to allow async
, throws
, or both of these effect specifiers to be marked on a read-only computed property or subscript:
// ...
class BankAccount {
// ...
var lastTransaction: Transaction {
get async throws { // <-- NEW: effects specifiers!
guard manager != nil else {
throw BankError.notInYourFavor
}
return await manager!.getLastTransaction()
}
}
subscript(_ day: Date) -> [Transaction] {
get async { // <-- NEW: effects specifiers!
return await manager?.getTransactions(onDay: day) ?? []
}
}
}
At corresponding access-sites of these properties, the expression will be treated as having the effects listed on the get
-ter, requiring the usual await
or try
to surround it as-needed:
extension BankAccount {
func meetsTransactionLimit(_ limit: Amount) async -> Bool {
return try! await self.lastTransaction.amount < limit
// ^~~~~~~~~~~~~~~~
// this access is async & throws
}
}
func hadWithdrawlOn(_ day: Date, from acct: BankAccount) async -> Bool {
return await !acct[day].allSatisfy { $0.amount >= Amount.zero }
// ^~~~~~~~~
// this access is async
}
Computed properties or subscripts only support effects specifiers if the only kind of accessor defined is a get
. The main purpose of imposing this read-only restriction is to limit the scope of this proposal to a simple, useful, and easy-to-understand feature. Limiting effects specifiers to read-only properties and subscripts in this proposal does not prevent future proposals from offering them for mutable members. For more discussion of why effectful setters are tricky, see the "Extensions considered" section of this proposal.
This section takes a deep-dive into the changes made to Swift and its implementation as a result of this proposal.
Under the grammar rules for declarations, under "Type Variable Properties", the proposed modifications and additions are:
getter-clause → attributes? mutation-modifier? "get" getter-effects? code-block
getter-effects → "throws"
getter-effects → "async" "throws"?
where getter-effects
is a new production in the grammar. This production allows one of the three possible combinations of effects specifiers between get
and {
, while enforcing an order between async
and throws
that mirrors the existing one on functions. Additionally, one can declare (but not define) an effectful property (such as for a protocol) by adding the effect keywords following the get
, as specified by this grammar:
getter-setter-keyword-block → "{" getter-keyword-clause setter-keyword-clause? "}"
getter-setter-keyword-block → "{" setter-keyword-clause getter-keyword-clause "}"
getter-keyword-clause → attributes? mutation-modifier? "get" getter-effects?
For example, one can write:
protocol Account {
associatedtype Transaction
var lastTransaction: Transaction { get async throws }
subscript(_ day: Date) -> [Transaction] { get async }
}
to enforce that a type conforming to Account
provides property and subscript witnesses that have the same or fewer effects than what is allowed by the protocol.
The interpretation of an effectful property definition is straightforward: the code-block
appearing in such a get
-ter definition will be allowed to exhibit the effects specified, i.e., throwing and/or suspending such that await
and try
expressions are allowed in that code-block
. Furthermore, expressions that evaluate to an access of the property or subscript will be treated as having the effects that are declared on that property. One can think of such expressions as a simple desugaring to a method call on the object. It is always possible to determine whether a property has such effects, because the declaration of the property is always known statically. Thus, it is a static error to omit the appropriate await
, try
, etc.
In order for a type to conform to a protocol containing effectful properties, the type must contain a property (or subscript) that exhibits the same or fewer effects than the protocol specifies for that requirement. This rule mirrors how conformance checking happens for functions with effects: a witness can be missing an effect, but it cannot exhibit an effect that is not accounted for by the requirement. Here is a well-typed example without any superfluous await
s or try
s that follows this rule:
protocol P {
var someProp: Int { get async throws }
}
class NoEffects: P { var someProp: Int { get { 1 } } }
class JustAsync: P { var someProp: Int { get async { 2 } } }
struct JustThrows: P { var someProp: Int { get throws { 3 } } }
struct Everything: P { var someProp: Int { get async throws { 4 } } }
func exampleExpressions() async throws {
let _ = NoEffects().someProp
let _ = try! await (NoEffects() as P).someProp
let _ = await JustAsync().someProp
let _ = try! await (JustAsync() as P).someProp
let _ = try! JustThrows().someProp
let _ = try! await (JustThrows() as P).someProp
let _ = try! await Everything().someProp
let _ = try! await (Everything() as P).someProp
}
Formally speaking, let us consider a getter G
to have a set of effects effects(G)
associated with it. This proposal adds one additional rule to conformance checking: if a getter definition W
is said to satisfy the requirements of a protocol's getter declaration R
, then effects(W)
is a subset of effects(R)
.
Effectful properties and subscripts can be inherited from a base class, and follow the usual visibility rules. The key difference is that, to override an inherited effectful property (or subscript) from the base class, the subclass's property must have the same or fewer effects than the property being overridden. This rule is a natural consequence of the subtyping relation for classes, where the base class must account for all of the effects that its subclasses may exhibit. In essence, this rule is the same as the one for protocol conformance.
Some API designers may want to take advantage of Swift's effectful properties by having an Objective-C method imported as a property. Objective-C methods are normally imported as Swift methods, so their import as an effectful Swift property will be controlled through an opt-in annotation. This avoids any source compatibility issues for imported declarations.
Due to the read-only restriction on Swift properties, and the fact that a large number of failable Objective-C methods are already imported as throws
methods in Swift, support for Objective-C bridging in this proposal is scoped for the Swift concurrency features. Importing as an effectful subscript is not included in this proposal. Furthermore, exporting effectful properties to Objective-C as methods are left to future work.
To import an Objective-C method as a Swift effectful property, the method must be compatible with the import rules for async
Swift methods, as described by SE-0297. An annotation changes this import behavior to produce an effectful Swift computed property, instead of an async
Swift method. The original ObjC method is still imported as a normal Swift method, alongside the property.
To summarize, an Objective-C method that meets the following requirements:
- The method takes exactly one argument, a completion handler, as recognized by SE-0297.
- The method returns
void
. - The method is annotated with
__attribute__((swift_async_name("getter:myProp()")))
. Note the use ofgetter:
to specify that it should be a property instead of a method.
will be imported as an effectful read-only Swift property named myProp
, instead of a Swift async
(and possibly also throws
) method. The following are Objective-C method examples from the SDK that have been annotated for import as an effectful Swift property:
// from Safari Services
@interface SFSafariTab: NSObject
- (void)getPagesWithCompletionHandler:(void (^)(NSArray<SFSafariPage *> *pages))completionHandler
__attribute__((swift_async_name("getter:pages()")));
// ...
@end
// from Exposure Notification
@interface ENManager: NSObject
- (void)getUserTraveledWithCompletionHandler:(void (^)(BOOL traveled, NSError *error))completionHandler
__attribute__((swift_async_name("getter:userTraveled()")));;
// ...
@end
which would be imported into Swift as:
class SFSafariTab: NSObject {
var pages: [SFSafariPage] {
get async { /* ... */ }
}
// ...
}
class ENManager: NSObject {
var userTraveled: Bool {
get async throws { /* ... */ }
}
}
The proposed syntactic changes are such that if they appeared in previous versions of the language, they would have been rejected as an error by the parser.
This proposal is additive and limits its scope intentionally to avoid breaking ABI stability.
As an additive feature, this will not affect API resilience. But, existing APIs that adopt effectful read-only properties will break backwards compatibility, because users of the API will be required to wrap accesses of the property with await
and/or try
.
In this section, we will discuss extensions and additions to this proposal, and why they are not included in the proposed design above.
Defining the interactions between async and/or throwing writable properties and features such as:
inout
_modify
- property observers, i.e.,
didSet
,willSet
- property wrappers
- writable subscripts
is a large project that requires a significant implementation effort. This proposal is primarily motivated by allowing the use of Swift concurrency features in computed properties and subscripts. The proposed design for effectful read-only properties is small and straightforward to implement, while still providing a notable benefit to real-world programs.
A key-path expression is syntactic sugar for instances of the KeyPath
class and its type-erased siblings. The introduction of effectful properties would require changes to the synthesis of subscript(keyPath:)
for each type. It is also likely to require restrictions on type-erasure for key-paths that can access effectful properties.
For example, because we do not allow for function overloading based only on differences in effects, some sort of mechanism like rethrows
and an equivalent version for async
(such as a "reasync") would be required on subscript(keyPath:)
as a starting-point. While a key-path literal can be automatically treated as a function, a general KeyPath
value is not a function, so it cannot carry effects in its type. This causes problems when trying to make, for example, a rethrows
version of subscript(keyPath:)
work.
We could also introduce additional kinds of key-paths that have various capabilities, like the existing WritableKeyPath
and ReferenceWritableKeyPath
. Then, we could synthesize versions of subscript
with the right effects specifiers on it, for example, subscript<T: ThrowingKeyPath>(keyPath: T) throws
. This would require KeyPath
kinds for all three new combinations of effects beyond "no effects".
So, a non-trivial restructuring of the type system, or significant extensions to the KeyPath
API, would be required to make key-paths work for effectful properties. Thus, for now, we will disallow accesses to effectful properties via key-paths. There already exist restrictions on key-paths to mutable properties based on the instance type (e.g., WritableKeyPath
), so it would not be unusual to disallow key-paths to effectful properties.
In this section, alternative designs for this proposal are discussed.
There are a number of places where the effects specifiers be placed:
<A> var prop: Type <B> {
<C> get <D> { }
}
Where <X>
refers to "position X" in the example. Consider each of these positions:
- Position A is primarily used by access modifiers like
private(set)
or declaration modifiers likeoverride
. The more effect-likemutating
/nonmutating
is only allowed in Position C, which precedes the accessor declaration, just like a method within a struct. This position was not chosen because phrases likeoverride async throws var prop
orasync throws override var prop
do not read particularly well. - Position B does not make much sense, because effects are only carried as part of a function type, not other types. So, it would be very confusing, leading people to think
Int async throws
is a type, when that is not. Introducing a new kind of punctuation here was ruled out because there are alternatives to this position. - Position C is not bad; it's only occupied by
mutating
/nonmutating
, but placing effects specifiers here is not consistent with the positioning for functions, which is after the subject. Since Position D is available, it makes more sense to use that instead of Position C. - Position D is the one ultimately chosen for this proposal. It is an unused place in the grammar, places the effects on the accessor and not the variable or its type. Plus, it is consistent with where effects go on a function declaration, after the subject:
get throws
andget async throws
, where get is the subject. Another benefit is that it is away from the variable, so it prevents confusion between the accessor's effects and the effects of a function being returned:
var predicate: (Int) async throws -> Bool {
get throws { /* ... */ }
}
The access of predicate
may throw, but if it doesn't, it results in a function that is async throws.
There was also a desire to take advantage of the implicit-getter shorthand for the above:
var predicate: (Int) async throws -> Bool { /* ... */ }
but there is no good place for effects specifiers here. Because this syntax is a short-hand / syntactic sugar, which necessarily has to trade some of its flexibility for conciseness. So, it was decided that it's OK to not allow effectful properties to be declared using this short-hand. The full syntax for computed properties explicitlys defines its accessors, and thus can declare effects on them.
The major difference for subscripts is the method-like header syntax and support for the implicit-getter short-hand, which combined make it look like a method:
class C {
subscript(_ : InType) <E> -> RetType { /* ... */ }
}
Position E in the above is a tempting place for effects specifiers for a subscript, but subscripts are not methods. They cannot be accessed a first-class function value with c.subscript
, nor called with c.subscript(0)
; they use an indexing syntax c[0]
. Methods cannot be assigned to, but subscript index expressions can be. Thus, they are closer to properties that can accept an argument.
Much like the short-hand for get-only properties, trying to find a position for effects specifiers on the short-hand form of get-only subscripts (whether its Position E or otherwise) will trap this feature in a corner if writable subscripts can support effects in the future. Why? Position E is a logically valid spot in the full-syntax and the short-hand syntax. Creating an inconsistency between the two would be bad. Then, using Position E + the full syntax creates an opportunity for confusion in situations like this:
subscript(_ i : Int) throws -> Bool {
get async { }
set { }
}
Here, the only logical interpretation is that set
is throws and get
is async throws. The programmer needs to look in multiple places to add up the effects in their head when trying to determine what effects are allowed in an accessor. This may not seem so bad in this short example, but consider having to skip over a large get
accessor definition to learn about all of the effects the set accessor is allowed to have for this subscript, when you do not need to do that for a computed property.
So, Position D was chosen as the one true place where you can look to see whether there are effects for that type of accessor, both for subscripts and computed properties.
The rethrows
specifier is excluded from this proposal because one cannot pass a closure (or any other explicit value) during a property get
operation.
The async
/await
feature is purpose-built for enabling asynchronous programming, so no consideration is given for alternative solutions that do not rely on that feature for asynchronous properties. The same reasoning applies to throws
/try
.
Thanks to Doug Gregor and John McCall for their guidance while crafting this proposal. The feasibility and design choices for this proposal were influenced by Becca Royal-Gordon's proposal for throwing property accessors and recent discussions with her.