SE-290 (second review): Unavailability Condition

Well, no actually, we have not enumerated all the possible design options. In fact, there is clearly the most intuitive option--albeit one that requires additional implementation work--which has not yet been put forward in this thread.

The core team's prompt for revision was specifically this:

The proposal needs to define the semantics for #unavailable for the platforms not specified and clearly explain how those semantics compose with the use of #available and other #unavailable annotations that may decorate a declaration.

The option that hasn't yet been mentioned here--although I sure hope that it was brought up during the revision pitch, but if so not by me--is to have no wildcard and to make if #unavailable(iOS 13) mean exactly what it says (in other words, on unstated platforms, iOS 13 is unavailable).

Composing would be intuitive: if #unavailable(iOS 13), #unavailable(macOS 11) would work exactly like if #unavailable(iOS 13, macOS 11), as users expect, and would not require further explanation or any warning; meanwhile, if #unavailable(iOS 13), #available(iOS 12, *) would also work exactly as expected and match if #available(iOS 12, *), #unavailable(iOS 13).

The implication, however, is that #unavailable(...) would not be merely the negation of #available(...). Syntactically that would be made manifest in that the former would forbid the use of wildcard * while the latter requires it.

Maybe this can all be resolved with a change of name. We’ve tied ourselves into knots over “what does #unavailable(iOS 14) mean on Linux?”, which is clearly a contentious question.

What if we wrote this instead?

if #before(iOS 14) {
  // do pre-iOS 14 fallback
}

This seems to me a much clearer spelling, and leads to more consistent interpretation. Few people would expect that any particular version of Linux is “before” iOS 14; most developers would probably (correctly) assume that the execution would not enter the block on Linux.

I know it’s late to bring up naming changes, but this reminds me of the debates over Int.isDivisible(by:). It wasn’t obvious what should happen in various cases (e.g. is 0 divisible by 5?), and that led to lots of disagreement. When the alternate spelling Int.isMultiple(of:) was proposed, it made all the edge cases much more obvious. Perhaps we’re in a similar situation here, chasing the wrong name.

5 Likes

It's explicitly out of the scope of this review.

But even if it were not, no change in the name alone can fix the counterintuitive behavior of composition.

Recall that the core team wished to address semantics for platforms not specified and how those semantics compose. Fundamentally, users will expect if #foo(iOS 9999), #foo(macOS 9999) to behave as does if #foo(iOS 9999, macOS 9999), whatever name we choose for #foo.

Without changing semantics, the fact remains that in the former case we have to justify to the user why magically the code they wrote will never be executed. At least an explicitly spelled-out wildcard might hint to the user that the composition of two #foo wildcards needs to be thought about explicitly.

* should be considered as part of NEGATE operation between #available and #unavailable.

If there is * in #available there is NO * in #unavailable,
vice versa
If there NO * in #available there is * in #unavailable.

  • !#available(macOS 11, iOS 14, *) == #unavailable(macOS 11, iOS 14)
  • !#available(macOS 11, iOS 14) == #unavailable(macOS 11, iOS 14, *)

In principle, !#available != #unavailable but

  • !#available(...,*) == #unavailable(...) :white_check_mark:
  • !#available(...) == #unavailable(...,*) :white_check_mark:

!* == un* cancel out each other to ∅, and !∅ == un∅ generate paired ⦰ -> *.

  • ∅ := empty set
  • ⌰ := reversed empty set
1 Like

I think one unfortunate issue of having the statement return true by default is that it would make it easier to fall into the problem scenarios. If we apply this to the example in the proposal, adding support to a new platform could silently cause the statement to fail, while with the wildcard there will at least be a compilation error in the #available portion of the code before you can reach the bug scenario. This would be the case for any alternative that is not literally the reverse of available, I think.

I'm not sure I understand why this is a problem scenario rather than exactly what's desired. If you want a behavior that's executed if iOS 13 is unavailable, then you would want it for all future platforms (other than what are considered descendants of iOS 13) and you would not want a future platform to cause a compilation error. What am I missing?

The case where you want to execute code only on pre-iOS 13 is almost certainly the case where you’re providing backward compatibility for that older OS. When you’re adding a new platform, you’re almost certainly not needing the backward compatibility behavior.

Put another way, it’s unlikely that I want legacy behavior to be the default.

(Edit: wow, autocorrect really messed that up. Sorry!)

4 Likes

Yes, I think that's a good way to put it. It returning false is also “safer” as the bug would be gated behind a compilation error in the #available portion. If it returned true by default, the bug would be introduced without any errors.

I see. You would like to use #unavailable to execute code where the named platform is available but the stated minimum version is unavailable.

The plain reading of #unavailable is that it's a feature for executing code where the given platform or version is unavailable. That has been my understanding of the feature, since it's how #available works: the whole point of the wildcard is to make the spelling match this understanding. We aren't even attempting to design the same feature, turns out.

I agree with you, then, that if what you state is the desired feature, then the already-approved named of the feature (#unavailable) is entirely misleading. I strongly believe that if we spell a feature as #unavailable for limiting code to platforms that are available, this will confuse an unbounded number of users to come.

2 Likes

I think in general * was a bad name choice for defining default platform behavior. Even in #available people's understanding of it is a happy coincidence.

I'm of the personal opinion that better docs would treat this (after all, this feature always existed in Obj-C and nobody seemed to have misused it back then), but it was clear from the first review that nobody could pinpoint precisely what the wildcard was for/represents.

3 Likes

The proposal says:

For context, this problem does not exist in Objective-C as if (@available(iOS 13.0, *) == NO) is a valid expression.

When I try that, Xcode tells me “@available does not guard availability here; use if (@available) instead”. Is the statement in the proposal incorrect?

Assuming this proposal is accepted, should we also open a Clang request to add an unavailability check to Objective-C to match Swift?

I apologize for posting to this thread after the review period has ended.

I realize much discussion has revolved around the wildcard character and its meaning regarding symbol availability. The only issue I have with the current proposal is the use of the wildcard character.

Although the wildcard in #available and #unavailable has a very precise meaning, a wildcard character in general has a meaning of ‘match everything’.

Even without understanding the precise meaning of the wildcard character, the fact that platforms other than iOS all resolve to true makes sense in the statement:

if #available(iOS 13.0, *)

The wildcard can be understood as ‘match or consider available every version of every other platform’. Therefore, for a non-specified platform this always resolves to true. I believe this is clear even to developers without deep knowledge of the mechanics of #available.

I understand the logic in the proposal that leads to the wildcard resolving to false. I also agree that non-specified platforms always resolving to false is the desired behavior.

if #unavailable(iOS 13.0, *)

However, I think people will find this spelling confusing, since a wildcard traditionally matches everything. I realize there is the suggestion to clarify this in the documentation, but I believe fighting against the widely understood meaning of a wildcard will lead to confusion.

Personally, I would favor a spelling without the wildcard:

if #unavailable(iOS 13.0)

I can think of this spelling as:

If compiling for iOS, this resolves to true if iOS 13.0 is unavailable. No other platform is referenced, either explicitly or via wildcard, so no other platform is considered and therefore no other platform can resolve to true.

Since there is no representation of other platforms via a wildcard, the unavailability statement simply doesn’t consider other platforms. Therefore the if statement is never true for any other platform.

This, I think would match with most developers understanding of how a wildcard works. If it’s there, every other platform matches. If it’s not there, no other platform matches.

I realize this is probably not the way the compiler implements or thinks about about platform matching. But I think the notion of ‘if it’s not mentioned in the if statement, it’s not considered’ is straightforward.

I also think that this spelling best reflects what the proposed behavior will be. Platforms not mentioned in #unavailable are not considered and therefore can't be true. Platforms mentioned via the wildcard in #available are always considered available and therefore are always true.

So, my preference would be for the ‘no wildcard’ spelling.

One Other Suggestion
I can see the point of view that someone could read the 'no wildcard' spelling as I’m on another platform, obviously iOS 13 is unavailable. In practice, I don't believe that will cause a great deal of confusion.

But, perhaps a spelling like the following would clarify the behavior:
if #unavailable(iOS 13.0, !*)

The presence of the wildcard character mirrors the notion that all versions of all other platforms are matched by the statement.

The presence of the negation symbol at the least lets the reader know something other than a plain wildcard match is going on.

It also strongly suggests the negation of the matching, that other platforms will return false and drop into the else block.

Between these two and the proposed solution, I prefer the spelling without the wildcard:

if #unavailable(iOS 13.0)

Again, I apologize for the late post.

5 Likes

Thanks for your throrough analysis James! I agree with your points, as all approaches can cause confusion in one way or the other, it makes sense to pursue the less problematic one. I agree that having no wildcard improves this situation and will probably not be too bad in practice.

I actually like this a lot! It makes sure one can't assume what's going to happen the platform due to the syntax being different, and we can justify it semantically by saying that availability lists are different from unavailability lists.

1 Like

For a language that brags about its fluency * was an odd choice to be used in #unavailable() An actual keyword like [all|others|allOthers|default] would have been better.

This has probably lasted as long as it has because most devs don't care. I write iOS software and not for other platforms. That * has never meant anything to me. It's just some requirement that the compiler needs. I paste it in because I have to but I don't have to know what it means. From reading this thread now I know what it means. @rockbruno you've certainly made clear that hardly anyone knows what it means and there's little or no documentation for it.

AFAICT the * serves no purpose in #available. Its meaning could be the default if it weren't present. If you need some other value then you need to be explicit anyway.

The default return value for #available() is true and the default return value for #unavailable() should be false.

I understand this ship has sailed and changing #available() is out of scope for this proposal. At any rate my opinion is that the best solution is to make a Standard library type that has available and unavailable methods that have normal method semantics. I always wrote functions for that sort of thing in my Objective-C apps. Maybe someone will write that.

Remark from the Review Manager

Hi everyone,

The review period for this proposal has technically ended, but I see there is some good signal still being generated in the discussion. The core team has not had a chance yet to discuss the outcome of this proposal review, but hopefully that will be soon. In the meantime, everyone should feel free to continue discussion here, particularly if it brings up new points that are salient for the discussion. I'm going to technically extend the review period out for a few more days to make it clear folks should feel comfortable with adding new signal if they have any.

Thanks!

6 Likes

Hi everyone,

The core team did talk about this proposal. There's a point or two I want to circle back on before posting a review resolution. I want to be transparent that I am a bit behind right now on a few things, but I wanted everyone on this thread to know that I have not forgotten about this and that I plan on posting a resolution to this review soon.

Ted

6 Likes

I'd like to start saying I'm quite late to this proposal and, while I've read it, I've only taken a quick glance to the forum thread. However, I'd like to take the opportunity to comment on this proposal before it gets approved/rejected. I apologize if this has already been expressed beforehand.

I feel this proposal has good value. Checking for availability is something we developers often do on Apple platforms. However, I also feel it only solves this problem for a specific use case: availability checks.

So I found myself asking the question: What about other conditions?

I would like to propose a different solution. I believe this would be a great example of a good use case for an unless control flow expression.

The following three examples are the same:

// if NOT in iOS 13, load the window.
// Post iOS 13 the window is loaded later in the lifecycle, in the SceneDelegate.
if #available(iOS 13, *) {

} else {
  loadMainWindow()
}

guard #available(iOS 13, *) else {
  loadMainWindow()
  return            // <-- mandatory return
}
// no-op

unless #available(iOS 13, *) {
  loadMainWindow()
}
// continue flow

Contrary to guard, unless would not require else nor require the flow to return or exit.

I'm aware unless has been suggested/pitched before [1, 2], even some suggested renaming guard to unless (which I would disagree with).

unless could also be implemented as a trailing expression. While this syntax is common in other languages, might not be welcomed by Swift developers:

loadMainWindow() unless #available(iOS 13, *)

In my opinion, it would make little sense to approve a proposal for a specific case like availability checks, and not a proposal for testing any negative condition with unless.

I did not expect someone to dig up that old idea of mine from the grave! For what it's worth, I wouldn't support it anymore now that I've gained a lot more experience with writing Swift. guard is much more than just an inverted if—it's a way to ensure a condition holds for the rest of the scope without increasing indentation, which is a sign of complexity. It keeps the handling of that edge case together with the condition itself, and makes clear that it is just that, an edge case, while the "meat" of the functionality is yet to follow.

Other conditions can already be inverted with prefix !, not requiring a special syntactic construct for inverted behavior. As far as I can tell, this proposal only came up because that this is not possible with #available, since the compiler would have to hard-code support for that boolean operation as well as any others that people would want (&& or ||, for starters).

TLDR: The reason we need "a proposal for a specific case like availability checks" is that #available is not a normal expression, which we could just negate.

1 Like

Sorry, I thought I made it clear renaming guard to unless was far from being suggested, and that I would disagree with that. I feel there is room for unless to live with if and guard, but that is a different conversation to have.

You are right, I was under the impression there were other special cases, but there are not, #available is the only one.

With that clarified, I changed my mind and support this proposal :+1:.

1 Like

Accepted with Revision

Aside: The review period for SE-0290 ended a while back, so this review conclusion is long delayed. For that, I apologize profusely both to the proposal author and all those that participated in the review discussion. When I took on managing the review I did not sufficiently anticipate how my time would be so pressed with other concerns. It is something I'll better assess in the future when scheduling reviews that I manage.

SE-0290 is accepted with revision. The sole revision is that the * syntax be dropped from the @unavailable predicate. There was a lot of great discussion that led to this insight. I'm particularly appreciative to @James_Dempsey nicely framing the rationale:

There was some general concern about what @available means for writing cross-platform code:

This was something I wrestled with as well, and was the primary reason it took me so long to circle back.

The @available feature, as it is designed today, is not sufficient to write all conditional logic to support platform-dependent code. Swift contains various #if predicates for this purpose, including #if os(...), #if arch(...), and so on.

The primary purpose of @available, as it exists today, is to be able to write code that can run on different versions of the same OS, and take advantage of newer APIs when they are available. The key example for Swift today is deploying apps to Apple platforms, where a downloaded app is designed to run on different releases of (say) iOS, but leverage different features and capabilities depending on the OS release. In that regards, this proposal addresses an ergonomic expressivity hole in the language to write such logic. The defined behavior in the proposal for the composition of #unavailable is conservatively defined as well, which will encourage @unavailable to be used in relatively easy to reason about cases.

The discussion on this proposal generated a tremendous amount of signal. There is a point where possibly the broader design of availability checking and cross-platform code could undergo a fresh re-think. In the meantime, SE-0290 is an incremental improvement in the existing model that will benefit users.

Thank you to everyone who participated in this review.

8 Likes