SE-290 (second review): Unavailability Condition

The second review of SE-0290, "Unavailability Condition", begins now and runs through January 22, 2021. This proposal was previously returned for revision:

The proposal is authored by @rockbruno.

For the second review, the goal is not to re-litigate the entire original proposal, but to focus on reviewing the important details of concern brought up during the previous review discussion that were specifically addressed in the revised proposal.

As always, thank you for your contribution to making Swift a better language.

Ted Kremenek
Review Manager

12 Likes

For those looking for an easy diff to read: Revision of Unavailability Condition by rockbruno · Pull Request #1220 · apple/swift-evolution · GitHub

3 Likes

IMO the discussion demonstrated that there's no resolution of the semantics of #unavailable that doesn't leave people confused and likely to make mistakes.

For that reason, I think we shouldn't proceed with this proposal.

(Instead, I'd suggest a future proposal that improves #available, and integrates both availability and unavailability checks into a unified syntax.)

2 Likes

The semantics of #unavailable are confusing. I agree the problem is worth solving but the current proposal muddies the waters even more and I'm strongly against it.

By way of example let's look at this code from the proposal:

if #unavailable(iOS 13, *) {
  // Symbol Availability: Default (deployment target)
} else {
  // Symbol Availability: iOS 13
}

What does this do on iOS 12, 13, and 14? It isn't clear from the syntax. It could mean we take the if branch for iOS <= 13. It could also mean we take it just for iOS 13. (I'm actually not sure which one it is as I write this).

1 Like

I think if we want to alter the proposal for confusion reasons, there must be a clear line between what's an confusing feature versus one that it isn't. If the argument is simply that someone who doesn't understand the syntax might be confused at first glance, I would argue that every single Swift feature would fall into that.

I think the line should be whether or not this confusion can lead to a bug, which doesn't seem to be the case here given that the type-checker will always watch your back. Remember that this proposal is not the one introducing availability specs, just using it.

1 Like

To be fair, this example seems fine to me. iOS 13 can't possibly be available if you're running iOS 12, and it will of course be available if you're running iOS 14. It's no different than #if available(iOS 13, *) { } else { ... }

The funny thing here is that actually both of the options you listed are wrong, it would take the if branch for iOS < 13, not iOS <= 13 and not just iOS 13.

To echo what others have written, that is the least confusing example. It is already well known to those who use #available checks that "<os> N" means version N and versions greater than N. So it's rather obvious that the inverse would be the same. i.e. that iOS 13 in your example clearly means that neither iOS 13 nor any later version is available.

So the if branch is taken for iOS 12 and below, and the else for iOS 13 and up.

Today, if we want to conditionally execute code on a version less than N, we need to write the following:

if #available(iOS N, *) {
  // do nothing
}
else {
  // do something
}

I would be happy if #unavailable let me invert that, so all I needed was:

if #unavailable(iOS N, *) {
  // do something
}

In other words, we should treat #unavailable(...) as meaning whatever condition would cause the else in the first snippet to be taken.

2 Likes

To complement for review purposes, the big problem we had with confusion came from trying to negate the individual components in the expression (like !iOS 13, !*), but this feature is meant to be read as !(expr), just like in !#available. So it should be read as an availability statement with an inverted result, a.k.a !#available a.k.a the else branch.

The proposal already goes into detail about how this is not true.

Wouldn't we solve the confusion by just having a documentation page saying something like this?

Equivalent to !#available -- the inverted result of checking if something is available.

It feels like the only reason people misread spec lists is simply because there's zero documentation about #available besides this Objective-C page that doesn't really explain how it works. This seems to have caused everyone to imagine their own wrong idea of how the calculation worked, which went unnoticed until this proposal attempted to use it in a different context.

As mentioned, the proposal already shows how the statement cannot be misused. Unless we can refute that, isn't this exactly what documentation was made for? It probably wouldn't hurt to also finally add a page for #available itself that properly explains the semantics once and for all.

2 Likes

IMO this would be a fantastic addition to Swift.

I think that any confusion surrounding #unavailable should apply equally to #available. Especially since #unavailable is intended to be the opposite of #available.

1 Like

"The statement cannot be misused" is too strongly stated. It's true that if #unavailable wouldn't change symbol availability, but that doesn't doesn't mean that you can't misuse it to do something that may not seem obvious. For example:

if #unavailable(iOS 13, *) {
  // Is it clear that this code does *not* run on macOS or tvOS?
  loadMainWindow()
}

The * in if #available(..., *) acts as a wildcard to say "all other platforms go here". The * in the proposal here is being used to say "all other platforms don't go here". #unavailable is pitched as simply the inverse of #available, but we've also moved the wildcard from one branch to the other, and it seems very unclear to me that this will be obvious to users.

I would be more inclined to remove the * entirely, since doing so would not change the behavior of the branch.

This is incorrect; the Swift documentation explains exactly how they work. The Swift Programming Language: Redirect.

1 Like

The reason the statement is "safe" in that case is that it's likely that this code will not even compile in a different platform in first place. If the code does compile, probably because we are dealing with a shared API, then misunderstanding the statement would cause the typechecker to fail for symbol availability reasons. (unless the statement is deliberately written to have nothing to do with availability, but then can you blame the feature?)

The wildcard is the minimum deployment target. The behavior of going to a certain branch is what it was intended to achieve, but it's not what it represents. We should stop seeing unavailability specs as it's own thing with its own terms, and more about the inverse of what would happen in availability.

Thanks for linking this! I didn't know about this one, but the point still stands. The documentation doesn't say what the wildcard is, just what it was intended to achieve. But it still makes sense from a semantic point of view, as the documentation about unavailability being inverse would probably leave no margin for confusion.

The motivating example given in the proposal itself has nothing to do with symbol availability reasons:

if #unavailable(iOS 13, *) {
  loadMainWindow()
}

The issue here is not whether the symbol for loadMainWindow() is available; rather, it's that the behavior of the platform has changed, and on iOS13+ you don't want to run certain code anymore. This seems to me to likely to be the main use case for #unavailable; if you wanted to use a symbol that was only available conditionally, you'd use if #available().

1 Like

I agree with this. I also agree with removing the * parameter from #unavailable.

The arguments against removing the * thread were discussed extensively in the preceding pitch thread, so I will not rehash them here.

Very briefly though, the behavior doesn’t pass the smell test. if #unavailable(iOS 13) has to mean what it says: “if iOS 13 is unavailable.” It is inexplicable that somehow being on macOS or Linux doesn’t trigger that branch when iOS 13 is plainly unavailable. If that is the best we can do for the design of this proposed feature, then it should be rejected in toto.

3 Likes

It's not clear to me that adding the * makes anything more clear. If anything, I think it makes it less clear.

I don't think any purpose is served by going around in the same circle again. It's up to the core team to decide now, and I only posted in this thread to document my opinion on whether the proposal should be accepted in its current form.

True, but this example would fall in the first issue: It wouldn't even compile on tvOS, because AppDelegate trickeries are iOS only. I can't see any legitimate reason to use unavailability in a complicated multiplatform scenario -- as you mentioned, this is designed to be used for very specific behavior changes in a platform, much like 99% of #available uses are simple API checks for a single platform.

This issue exists in theory, but there doesn't seem to be a way for a developer to actually fall into of them. These scenarios involving multiplatform code are complicated even for #available because multiplatform apps are by definition complicated. I can't stress enough that these scenarios are not what this feature is built for, and 99% of the developers won't experience them.