My point is that if one doesn't want to use a language feature for one reason or another that doesn't mean that it shouldn't be available. I, personally, don't like functional programming very much because it feels (I use the term "feels" because I haven't done benchmarks and I haven't researched Swift's optimization runs to claim it as fact) overly wasteful of computing resources due to profuse usage of closures and repeated allocation of temporary arrays, but I don't argue against adding functional-style utilities like map
, flatMap
, compactMap
and the entirety of Combine
, which I'll happily throw away from my code once I get my long-awaited (pun intended ) concurrency model.
For me a stored property and computed property should be interchangeable.
If we introduce async get computed property then async stored properties should be supported. (Not sure what this would look like) This proposal should address stored properties async getter to be able to participate in this async world (maybe via some kind of async property wrapper) along with support for setter.
But if doesnāt make sense for stored properties to support async then I donāt believe the asymmetry is worth it only for get computed properties.
It does makes sense for a @Sendable
stored actor property. To access it from outside the actor you need to go through the async mailbox.
Very much +1 from me. When updating a library to use the new concurrency features a few months ago I immediately felt the absence of async
getters; I wanted to expose some properties from types which had newly become actors but didn't want to convert everything to function calls since the getters were just acquiring a lock, reading and possibly lightly processing a value, releasing the lock, then returning. Given I'd just expected that I'd be able to expose async properties and was surprised to find I couldn't, this (IMO) obviously deserves inclusion in Swift.
The
rethrows
specifier is excluded from this proposal because one cannot pass a closure (or any other explicit value) during a propertyget
operation.
However, you can pass a closure to a read-only subscript, either as an argument within the square brackets, or as a trailing closure.
Stored properties on actors are implicitly async when used from other actors. That's one of the whole ideas here. We already (as part of the actor proposal) need to write await thing.property
, but we have no way to declare an async property
on general types. This proposal fills in that hole (among many other benefits listed above).
-Chris
I have been thinking why I am on "this adds complexity" side.
The quotes borrowed from @OneSadCookie and @Panajev were the very reason.
One of "the line" for me is: Complex logic should be written as a method.
However, this is what Swift is today already:
var count: [Element] { ... }
func count() -> [Element] { ... }
Apparently both of the above are correct today very obviously, and it's up to developers which to choose.
And that process itself is doesn't get affected by this particular proposal.
Which means I couldn't come up with a concrete or convincing enough explanation ā I think am positive with this proposal now.
And I think the place of "effects specifiers" is very precise and nice.
I think this is very necessary in order to make async
APIs feel at home in Swift. I'm a bit less sure this is necessary when it comes to throwing, but I can't really find a reason to object.
My only concern with the syntax is: how does it evolve in protocol declarations? Currently we allow this:
protocol P {
var p1: Int { get }
}
With this proposal we'll allow this:
protocol P {
var p1: Int { get async throws }
}
And maybe in the future we'll allow this:
protocol P {
var p2: Int { get async throws nonmutating set }
}
I feel it's a bit hard to tell where the get
declaration ends and when the set
declaration starts now that some attributes go after the get
, but I don't really see how to improve it without bringing other inconsistencies. And we still haven't added typed throw to the keyword soup either.
I'm not concerned enough by this to say the current proposal needs another syntax, but it remains on the back of my mind that we may eventually have to do something about it.
@beccadax's proposal mentioned the alternative to put commas in protocol accessor declarations. Since it's currently possible to split get
and set
on different lines, I'd prefer allowing the semicolon ;
between them. If we end up with multiple thrown errors using a syntax along the lines of
func foo() async throws E1, E2 -> Int { ... }
the comma may clash. So the following options would be equivalent:
protocol P {
var p1: Int {
get async throws
nonmutating set
}
var p2: Int { get async throws; nonmutating set }
}
I agree we should support a separator in the protocol requirement specification.
However, get and set are more like items in a list than they are statements. I think it would be slightly better to use comma instead of semicolon here.
Good point. I can also imagine some use-cases for this too:
extension Array {
// select possibly non-contiguous members based on predicate function
subscript<T>(matching: (T) throws -> Bool) -> [T] { /**/ }
}
It will need to align with the fix for rethrows
and since its new, would not support rethrows(unsafe)
.
I'll look into the feasibility of implementing this.
What is your evaluation of the proposal?
+1
Is the problem being addressed significant enough to warrant a change to Swift?
I like Xiaodi Wu's phrasing: significant and timely.
Does this proposal fit well with the feel and direction of Swift?
There seem to be two broad opinions on this; I'm on the "yes" side. In terms of mental complexity, I think the mental model "properties and subscripts can have effects like methods can (here is how you write that down)" is less complex than "effects are things that methods can have; properties and subscripts can't do that". (I see the getter-only situation as a temporary stopping point on the way towards the more general model.)
But I'm a bit concerned about the keypaths story. Am I reading the proposal right that it would be impossible to form keypaths (of any kind) to or through an effectful property?
I feel like keypaths are a particularly elegant Swift feature, in their simplicity and general applicability. This exception would complicate that story significantly. If I update some property to be async
it's natural to expect I will need to apply some await
; it would be a more unpleasant surprise to discover that I can no longer pass that type into my nice generic utility that happens to use keypaths as an implementation detail.
(I do see that even if we had keypath support what my utility does with the keypath would have to change, but unlike adding await
the only solution available seems to be rearchitecting away from keypaths entirely: that seems to push people away from the nice Swifty abstraction that keypaths offer.)
If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
n/a
How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
Careful reading of the proposal & this thread; I've followed the concurrency proposals in general but not put any code to the test.
As a feature I think itās fine. I donāt see anything wrong with examples like try await asyncSequence.count
, in fact I think the language would suffer quite a bit under the inconsistency not having it would bring.
As for the idea that computed properties are supposed to be O(1), that can still hold, assuming that O(1) simply means āconstant timeā. It just may happen to be a relatively large constant.
One thing Iām not sure about is whether we should be able to async
just any old getter, or whether async getters must be in an actor?
I went into this skeptically (āread only effectsā sounds like an oxymoron) but Iām now a pretty clear +1
Overall, +1.
One thing that concerns me is the adaptation to KeyPath in the future.
a non-trivial restructuring of the type system, or significant extensions to the KeyPath API, would be required
As you stated in the proposal, I also think it will be quite difficult to adapt to the current KeyPath API in terms of the current type system.
For example, with the current KeyPath API, it would be combinatorial explosion to express async
getter, throws
getter, and async throws
getter with mutability and reference/value semantics. (And needs to care subtyping structure)
To avoid the explosion, one possible way would be to define trait protocols such as protocol WritableKeyPath<T>
and protocol ReferenceKeyPath<T>
and make it composable like WritableKeyPath<T> & ThrowingKeyPath<T>
, I think. But this needs significant efforts in type-system.
@kavon So I would like to know what features do you think are needed in the type system to express the KeyPaths? And do you think it is feasible?
The
rethrows
specifier is excluded from this proposal because one cannot pass a closure (or any other explicit value) during a propertyget
operation.
This is not correct; rethrows can be calculated by conformance. Consider the following:
@rethrows
protocol SomeSourceOfThrowing {
func item() throws -> String
}
struct Container<Source: SomeSourceOfThrowing> {
var source: Source
var something: String {
get rethrows {
try source.item()
}
}
}
Non theoretical accessors would be for example:
extension AsyncSequence {
var first: Element? {
get async rethrows {
...
}
}
}
The rest of this proposal seems fantastic and is something that will definitely provide some great positive impact to frameworks and apps written in swift.
I know this has already been accepted, but I didn't see any discussion of lazy async properties (or throwing properties) in either this thread or the proposal. lazy var foo() = await bar()
doesn't seem to work in the compiler I'm testing ("'async' call cannot occur in a property initializer"). Is this a known limitation, an oversight or a bug?
Personally, I think being able to express the following in a more succinct (and safer) manner would be advantageous. Especially since lazy
is often used to defer expensive calculations, a use-case await
relates to as well.
private var _foo: Foo?
var foo: Foo {
get async throws {
if _foo == nil { _foo = try await makeFoo() }
return _foo!
}
}
UPDATE: Actually, I'm not even sure the above is sufficient. For non-actor types, you might need to create a synchronization context as well, perhaps something like:
private struct FooActor {
func get(_ fooArguments: @autoclosure () -> FooArguments) async throws -> Foo {
if foo == nil { foo = Foo(fooArguents()) }
return foo!
}
private var foo: Foo?
}
private lazy var fooActor = FooActor()
private var _foo: Foo?
var foo: Foo {
get async throws {
if _foo == nil { _foo = fooActor.foo(FooArguments()) }
return _foo!
}
}