SE-0310: Effectful Read-only Properties

I'd like to shed some light on my reasoning for the syntax chosen in this proposal.

First, let's consider some of places for effects specifiers that have been discussed or considered for a computed property:

<A> var prop: Type <B> {
  <C> get <D> { }
}
  • Position A is primarily used by access modifiers like private(set) or declaration modifiers like override. The more "effecty" mutating/nonmutating is only allowed in Position C, which precedes the accessor declaration, just like a method within a struct. Also, I don't think there's a happy ordering of effects specifiers and the other modifiers in Position A: neither override async throws var prop nor async throws override var prop reads well to me. For functions, the effects specifiers appear after the subject, not before it.

  • Position B doesn't make any sense to me, because effects are only carried as part of a function type, not other types. I think it would be very confusing, leading people to think Int async throws is a type, when that is not. Strega had suggested we use an arrow syntax, such as var prop async throws -> Bool { }, but the arrow would suggest that accessing the property yields a function, when it will be a boolean. This gets even more confusing for properties that are functions. At first glance, var predicate async throws -> ((Int) async throws -> Bool) { } looks like it returns a curried function.

  • 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 an unused place in the grammar, places the effects on the accessor and not the variable or its type, and is consistent with where effects go on a function declaration, after the subject: get throws and get async throws, where get is the subject. In addition, this position 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. I can understand the desire to take advantage of the implicit-getter shorthand:

  var predicate: (Int) async throws -> Bool { /* ... */ }

but there's no good place for effects specifiers here, and I think that's OK. It's a short-hand / syntactic sugar, which necessarily has to trade some of its flexibility for conciseness. The full syntax for computed properties explicitly defines its accessors.

Now, let's move onto subscripts. I'll skip right to the major difference for subscripts, which is that they have a method-like header and support the implicit-getter short-hand, so that it resembles a method too:

class C {
  subscript(_ : InType) <E> -> RetType { /* ... */ }
}

Position E 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, I think 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 us in a corner once writable subscripts can support effects. Why? Position E a logically valid spot in the full-syntax and the short-hand syntax, and 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, I think that Position D should be 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.

15 Likes

This type of option would put all of my concerns to rest.

I believe most programmers will choose this route. And this option will also force the future setters proposal to require declaring setters and getters the way this proposal describers getters, which is how things will go with this current proposal anyway.
But the end result would be easy to understand code, in most projects, when it's a get-only property and it will be strongly apparent that something complex is happening if the setter and getter are both implemented.

I'd be very happy and confident in the future if something like this were added to this proposal from the start. The exact syntax of the sugaring can be debated, but the benefits of having this option from day 1 would not be trivial.

I'm not sure what you mean here, Strega.

Just to be clear, the second example is a valid, non-effectful computed property today, that returns a function with type (Int) async throws -> Bool. It's meant to demonstrate that there is no place to add an effects specifier for the property predicate's get access, because that second example uses shorthand that for the accessor's declaration.

What's OK about that example is the fact that shorthand syntax is not meant to be flexible; it's to be concise. So, I do not think we should support effects specifiers on computed properties that use the implicit-getter shorthand syntax, which is counter to what you were suggesting earlier.

Oh, yes I see it now. My mistake. I try to avoid this type of shorthand because it's hard for me to read and I am not used to seeing it. As you pointed out the number of options we have for these declarations can become confusing. Which is why I definitely appreciate the proposal as is, it removes this type of confusion.

I apologize for drawing my own conclusion without fully understanding your point.


I just think it would be a shame to lose the elegant simplicity of get-less get-only properties.

With concurrency becoming so much easier, this proposal could potentially make writing the get in a get-only property the norm since it would be required for async and potentially throws anyway. If I personally had to write it for those cases I would write it for all cases to make sure my code easy to follow.

As for subscripts, I have no strong opinion. I use them so infrequently as it is that I have to look up the syntax when I want to implement them.

1 Like

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 :slightly_smiling_face:) 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.

1 Like

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.

Alternatives considered

The rethrows specifier is excluded from this proposal because one cannot pass a closure (or any other explicit value) during a property get 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.

9 Likes

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

13 Likes

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.

4 Likes

@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 }
}
5 Likes

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.

3 Likes

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.

1 Like

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

2 Likes

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 property get 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.

2 Likes

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!
  }
}
1 Like