SE-0310: Effectful Read-only Properties

Agreed. And ah, yes that is it, "mental load" is what I'd wanted to say.

I don't think these alternative sugared syntaxes should be available:

  • what it's throwing and/or asynchronous is the getter, not the variable per se;
  • async var user and async let user would have very different meanings and diagnostics;
  • a variable without effects is easier to get/set at the use-site; having a sugared syntax that resembles the way throwing and/or asynchronous functions are spelled, may lead unexperienced users to define throwing and/or asynchronous setters when it shouldn't be needed;
  • in protocols, users would still need to type async and throws near get and set, regardless of the sugar syntax;
  • if we agree now that async var user means that only the getter is asynchronous, then it would be a breaking change to have it to mean that both the getter and setter are asynchronous.

I'm really +1 to this pitch and how it has been structured. This has been one of the first wanted features (SR-238 has been filed in 2015 and it is 19th among all the 15,399 issues filed on bugs.swift.org ordered by number of votes).

By your logic, Swift should've stuck to classical variable versus function approach where any variable/property has its own storage and if any run-time behavior is required, then it should be replaced with two methods: a getter and a setter. The "mental load" argument is assuming that the programmer is expected to have mental capacity issues. Java was motivated by "keeping it simple" and that's why it became a purist religiously-object-oriented language that gets in your way more than it helps you.

Java kept it simple? :face_with_raised_eyebrow:, not the expert in the language but I guess there is simple and simple :).

Again, if Java failed at that goal it does not mean the goal is impossible… if Java and C++ are the only two patterns possible ahead of us do we have C++ in our future in terms of needing language lawyers?

I find the argument dismissive of mental load on developers a bit odd and gatekeepey.

Furthermore, there would need to be a reason in that discussion for why this preference differs specifically in the case of properties with effects. "Why not just use a method" is a reason to not expose computed getters at all, but is not an interesting argument to have because they exist in the language and are not going away. There needs to be a substantive reason offered for why collection.count is fine, but await collection.count is not fine and should be spelled await collection.count().

Similarly, "this adds complexity" needs to explain why properties that can throw is particularly more complex than methods that can throw. There seem to be some really bright lines being drawn here in arbitrary places based on reasoning that is not explained.

15 Likes

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
Terms of Service

Privacy Policy

Cookie Policy