Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager by email or DM. When contacting the review manager directly, please keep the proposal link at the top of the message.
What goes into a review?
The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:
What is your evaluation of the proposal?
Is the problem being addressed significant enough to warrant a change to Swift?
Does this proposal fit well with the feel and direction of Swift?
If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
More information about the Swift evolution process is available at:
Can anyone speak to why availability conditions can be supported but not general conditions? They both violate the single return type rule, so what makes availability so special here?
A function returning an opaque result type is allowed to return values of different concrete types from conditionally available if #available branches without any other dynamic conditions, if and only if, all universally available return statements in its body return a value of the same concrete type T . All returned values regardless of their location must meet all of the constraints stated on the opaque type.
This seems to be restricting the if statement that the if #available check is in, but of course an if #available check can be nested completely within another if:
if foo {
if #available(greenOS 1000) {
return Y()
}
}
return X()
The reverse is true as well: an ordinary if can be nested within an if #available, and the two branches of that if (if they contain returns) must return the same type:
if #available(greenOS 1000) {
if foo {
return Y()
} else {
return X()
}
}
return X()
Falling out of an if, or not taking it, has similar implications to nesting:
if foo {
return X()
}
if #available(greenOS 1000) {
return Y()
}
return X()
or
if #available(greenOS 1000) {
if foo {
return Y()
}
}
return X()
And of course you can do the same with any other conditional control flow: guard, switch, for, etc. I think the rule just has to discuss the possibility of constructing a static decision tree.
Should the proposal just say that it's allowed if the implementation can construct a static decision tree for the result type, but that the situations in which that's guaranteed to work can expand over time? Or will we need to put any future expansions through separate evolution review?
The underlying type of an opaque type must be constant during the execution of a process. Availability conditions do not change mid-execution, which is what we're taking advantage of here. Conceivably this could be extended to other known-constant conditions, like global let bindings or @const expressions, in the future as further proposals.
guard examples would also be a good idea, even if many users can easily apply the rules either way. I think it's slightly odd the proposal calls out if conditions and not just general conditions.
I think we'd have to put any expansions through a separate review. I'll adjust the sentence to be explicit that if #available cannot be nested in other dynamic conditions or have nested dynamic conditions in it.
Seems like something which SwiftUI would need for pretty unremarkable purposes. Part of the point of returning opaque types all over the place is so that they can change them to different types without breaking clients, but without this they can't actually switch to a new type which sort of defeats the purpose.
It'd be neat to have this generalized to any @_effects(readonly) (or something more appropriate if that isn't it) condition. I can imagine things like switching to a debug version of some type depending on if an env variable is set at startup, where the condition isn't @const-suitable but we can promise that it'll never change. I assume that'd be dramatically more complicated than this proposal, though, and it's not like #available being special and different is a new thing.
In particular, the function "asRectangle()" doesn't return a Rectangle - neither statically (the return type is some Shape) and not necessarily dynamically (it could just return self).
It could be clearer. It could be any function which returns an instance of OldThing(...) or NewThing(...) depending on availability, with that difference being hidden behind an opaque type.
The example demonstrated by the proposal does not make it particularly clear, but this seems like a useful feature towards making APIs back-portable by 1st or 3rd parties.
For a trivial example, I could think of ways to make SwiftUI’s refreshable(action:) modifier available (albeit in a renamed form[1]) to older platform versions without resorting to SwiftUI._ConditionalContent the actual return type of the modifier:
extension View {
// Note: no `@ViewBuilder` here!
@available(iOS 14.0, *)
func refreshable_(action: @escaping @Sendable () async -> Void) -> some View {
if #available(iOS 15.0, *) {
return self.refreshable(action: action)
}
// Something like https://github.com/siteline/SwiftUI-Introspect#scrollview
...
}
}
(If the function interface to backport contains data types or protocols unavailable to older targeted platform versions, however, then I'm afraid something more is needed than this proposal alone.)
What is your evaluation of the proposal?
+1
Is the problem being addressed significant enough to warrant a change to Swift?
Yes.
Does this proposal fit well with the feel and direction of Swift?
It seems like a natural extension to the rules about #available (and #unavailable).
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?
Quick reading, but also gave it a quick try on the latest main snapshot toolchain using iOS Simulator 14.5 and 15.5.
Completely shadowing the existing implementation would further need a way for the shadowing function’s body to refer back to the shadowed one in the SwiftUI module in a qualified form, say, return `SwiftUI.View`.refreshable(action: action). Something similar was discussed but mainly for module-level symbols, not members, in Pitch: Fully qualified name syntax. ↩︎
No, the semantics are changing only for opaque result types returned by functions, so an attempt to assign different types in your example would still fail to type-check.
The proposal has been updated to clarify the rules regarding placement and semantics of if #available conditions and provide more examples of well-formed and invalid uses. I'd like to thank @John_McCall for all the help with this!
I view this proposal not from the back porting perspective but rather as a door opener for transparent under the hood improvements.
func swiftUIScrollView() -> some View {
If #available(/* later os version */) {
return NewSwiftUINativeSrollViewWithAmazingPerformance()
}
return OldScrollViewBakedByUIKit()
}
This would basically allow the framework maintainer to replace the underlying type in a safely manner and apply bug fixes.
+1 for me.
I have one question though. Are @frozen types doomed from such improvements or can they still be updated and improved somehow?
One concrete example would be the State type in SwiftUI, that type isn‘t lazy unlike its object counterpart StateObject which can be somewhat an annoyance . I‘m not 100% sure if that proposal would enable anything to flip some internals of that type though.
A local variable with opaque type needs an initial underlying type at the declaration, and this proposal does not suggest changing that. I assume it would work if you assigned it to a closure that was immediately called if there were a way to specify that the closure returned some P, e.g.
let v: some P = {
if #available(...) {
return P1()
} else {
return P2()
}
}()
I think this is an interesting functionality but I find the syntax very odd. It's not going to be clear as a developer when I can send a different type.
For instance going from
func test() -> some Shape {
if #available(macOS 100, *) { ✅
return Rectangle()
}
return self
}
to
func test() -> some Shape {
if cond {
...
} else if #available(macOS 100, *) { ❌
return Rectangle()
}
return self
}
my code won't compile.
But what's going to be the error message? If it's "return statements in its body do not have matching underlying types" then it's misleading because it was the case just before when using only #if.
I find even less clear when reading the guard example.
Shouldn't the restriction be on the function itself? And Swift have a way to choose the right definition to use based on the availability?
// swift use this version of `test` when on macOS >= 100
@available(macOS 100, *)
func test() -> some Shape {
// now you can put whatever condition you want
guard let x = <opt-value> else {
return ...
}
return Rectangle()
}
// swift use this version of `test` when on macOS <100
func test() -> some Shape {
guard let x = <opt-value> else {
return ...
}
return Square()
}
You might have a little bit more duplicated code but at least what happen is clear IMO.
Edit: re-reading the proposal the availability check is done at runtime. So I think it's the right way. Just maybe make sure to have a clear error message on why it will fail to compile in some use cases.