SE-0310: Effectful Read-only Properties

I can see why people think this ought to be written as part of the var / subscript type, but I think the better precedent here is mutating / nonmutating, which have to be written as part of the individual accessor.

16 Likes

Ah! Thank you for that missing link in my mental model. This is making sense now.

I'm on board now.

I would however still very much appreciate a sugared alternative, like one of these:

async var myProperty: throws Bool {}
async var myProperty throws: Bool {}
throws async var myProperty: Bool {}

Edit: I'm really not on board... I want the features, but I just don't like the approach at all. One of the above should just be the way it is. A property is either async or it's not. Having a setter in the future should also be async or not. Throws should be absolute as well. If the property throws the getter and setter should be throwing.

You’ve just contradicted your own conclusion.

If a property is marked with throws, then both the getter and setter should throw. But the setter cannot throw: that is not supported. Therefore, the property cannot be marked with throws.

1 Like

+1 for subscripts
-1 for computed properties.

I'm just trying to consider the future here. If a property can ever be throws at all I don't see why the setter won't hypothetically be able to fail in a future where setters get a proposal of their own.

I'm just saying, we should consider right now whether properties should be able to be split into different underlying functions; even though this proposal is specifically for get-only. The solution of this proposal heavily implies setters could be different someday, but if they never will be different than decorating just the get { is the wrong thing to do.

Since a get only property can't have a setter, this proposal would function the same if the decorations were directly on the var declaration. Adding setters later would not be a problem as throws would always indicate you'd need try to use the property, getting or setting. async is more confusing but I imagine that will always remain get-only as the set operation would either be synchronous or detached.

2 Likes

Thanks for taking the time.

One of AVAsset's potentially blocking computed properties is var duration: CMTime . If the asset isn't downloaded already, it will block, so it's still O(1) amortized.

This kind of example seems to me to be missing the point. Like, yes, you could design an API (and apparently AVAsset is designed this way), but why would you want that, as the consumer of an API? I can't imagine a situation where I'd not care whether the access of duration might block indefinitely.

In general, implicit caching causes headaches. UIViewController has a similar problem (though synchronous): view with its implicit caching behavior is almost never what you want. viewIfLoaded and loadView() are a more useful API surface. I find it better to model the state transition explicitly in the type system. For example, you might have

class LoadedAsset {
    let duration: CMTime
}
async func load() -> LoadedAsset

or if it's a kind of partial stateful streaming thing, you might go to the UIViewController approach:

struct Properties {
    var duration: CMTime
}
var propertiesIfLoaded: Properties?
async func load() -> Properties

I guess it seems more to me that "the design of AVAsset is wrong" than "this proposal solves a real problem for AVAsset".

2 Likes

I'm also sort of debating with myself whether it would be preferable to just say e.g.

async var user: LoggedInUserDetails { ... }
throwing var value: T { ... }

(realizing that "throwing" would probably have to be "throws")

Keep it simple. But on the other hand it would kinda be nice if only the setter could throw and you could use the getter as any other normal property... ¯\_(ツ)_/¯

Using e.g. "async var user: ..." with the current constraint to getter only could be solved by simply disallowing a setter for those kinds of properties for now, as far as I can see.

1 Like

+1

I disagree with the sentiment that these use cases are better expressed as methods. This is a step forward toward making properties more powerful and expressive. I can imagine a future proposal that will involve throwing and/or async mutable properties. The language should not dictate the programmer how to get things done, but should provide sufficient tools to enable the programmer to make that decision. Allocating buffers are "better done" using ContiguousArray, but Swift still provides UnsafeBufferPointer.allocate.

3 Likes

In the future if we might introduce typed throws and it would more ergonomic to have the effects at the accessors level.

var property1: MyType {
  get { ... } // equals `get throws Never`
}

var property2: MyType {
  get throws MyError { ... }
}

In general I'm +1 on this feature.

2 Likes

The thing is that this is added complexity and mental load for devs as properties and methods become less and less easy to distinguish as the properties grow in power and versatility. Sometimes less is more and modern Swift seems sometimes to favour of adding features on top of features to make some things more concise and value flexibility and power over simplicity and local reasoning.

It does not answer the question of why properties should be more powerful and expressive, it takes it as a given without any possible side effects.

2 Likes

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