Platform Aliases

Motivation

Scanning through the Swift Standard Library at a high level and looking at some of the newer additions, we can see something like this:

@available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *)
@frozen
public struct DiscontiguousSlice<Base: Collection> { ... }

@available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *)
extension DiscontiguousSlice { ... }

@available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *)
extension DiscontiguousSlice.Index: Hashable 
    where Base.Index: Hashable {}

@available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *)
extension DiscontiguousSlice: Collection { ... }

There is lots of repetition here, as I'm sure anyone who's written many of these availability checks will sympathise with. It also could be seen as a "leaky abstraction" of the underlying target platforms. As I understand it, the availability checks are necessary on ABI stable platforms to ensure that the standard library features are available on the target platform, because the platform itself provides its own copy of the Swift runtime and standard library. This informs the compiler to enforce uses of #available / @available as necessary to prevent compilation in the event that an unavailable call is made.

However, we see above that this becomes very repetitive very quickly. Additionally, as Swift reaches new platforms, it seems like this level of "connection" to a given vendor's specific platforms is somewhat restrictive and will only get worse as more platforms are added.

Proposed Solution

The least disruptive and backwards compatible solution appears to be Platform Aliasing, which can reduce an annotation like this...

@available(macOS 10.16, iOS 14.0, watchOS 7.0, tvOS 14.0, *)

...to something as simple as this...

@available(apple 2020, *)

The apple platform name in this example corresponds to a "Platform Alias" which implicitly references any number of other platforms. Versions of the alias then correspond to specific versions of the underlying platforms. The versioning example here is year-based, with the minor version indicating the iteration of the software released for that year. Any other alternatives are also possible, of course. Pseudocode:

apple <- macOS, iOS, watchOS, tvOS

apple 2020.1 <- macOS 10.16
             <- iOS 14.0
             <- watchOS 7.0
             <- tvOS 14.0   

## other platforms still use 2020.1 versions
apple 2020.2 <- iOS 14.1

Examples:

// uses 2020.2 versions
@available(apple 2020.2, *)

// uses 2020.1 versions
// (earliest supported platforms for this year)
@available(apple 2020, *)

This works especially well for Apple platforms as software releases of their respective platforms are generally released at similar times supporting the same new features as each other. I imagine these Platform Alias definitions will have to be hard-coded into the compiler (i.e. they are not user-configurable) but seeing as platform definitions themselves are non-configurable from user code, this is not a real negative (given that sensible defaults are provided by the platform vendors).

Apologies if something similar to this has been pitched before, I had a good look and was unable to see anything. Please let me know your thoughts or if I'm coming at this from totally the wrong angle!

8 Likes

Yes, @Erica_Sadun has pitched a “vendor” availability check. Her pitch didn’t include versioning but it certainly is a logical extension:

2 Likes

I'd like to see this too. Though I'm not sure your alias subversions are applicable to anyone but Apple, and their releases don't always line up capability wise. e.g. iOS 14.2 may ship alongside watchOS 7.1, tvOS 14.2, and macOS 11.0.0. Might need some more refinement for that part of the proposal.

Absolutely agreed. Perhaps the proposal should instead allow for directly checking availability against the standard library (or other framework) version itself. Maybe this would be easier than aliasing the given vendor platforms? It would also achieve similar results while being totally decoupled from host vendors or platforms.

@available(stdlib 5.3)
struct ThingInTheStandardLibrary {}

// stdlib could have an alias "Swift"
// this would also remove the need for `*` in this instance
if #available(Swift 5.3) {
    let thing = ThingInTheStandardLibrary()
}

// third-party library support
@available(UIKit 14.0)
struct SomethingNewInUIKit {}

I'm unsure how feasible this would be compared to the original proposal though.

4 Likes

I suspect that there would be resistance within Apple to encoding assumptions about their release cycle in a language or SDK feature. However, the ability for a module to create platform aliases for its own use would be helpful.

Expanding versioning to third party frameworks would be a first step towards versioned alias for those frameworks. Perhaps that's a better starting point.

2 Likes

IMO this is the correct way to do it for almost all cases, but Apple don’t do semantic versioning, so we’re all left with this poor user experience.

Not all versions of frameworks are the same across all platforms, so it wouldn’t entirely remove platform checks, but it would still be a more accurate API availability model than we have now.

So I see this as an issue for Apple, to improve the developer experience using their SDKs.

I have been working on something similar to this: [Sema] Define availability specification macros with a frontend flag by xymus ¡ Pull Request #33439 ¡ apple/swift ¡ GitHub

It has been merged in main but it's not in Swift 5.3. At this point it’s a frontend flag that needs to be defined in each project. We're still looking for how we can make it more practical to use, so this thread is very interesting!

5 Likes

Yes, the ever‐growing availability lists are getting cumbersome. However, the baked‐in availability examples suggested above would only cover some of the use cases I have encountered.

Why not a full‐fledged alias system comparable to type aliases:

availabilityalias CollectionDifference = (macOS 10.15, tvOS 13, iOS 13, watchOS 6)

func compareCollections() {
  if #available(CollectionDifference, *) {
    let x = a.difference(from: b)
  } else {
    // ...
  }
}

@available(CollectionDifference, *)
extension CollectionDifference {
  // ...
}

Other repetitive examples I have wished to shorten:

availabilityalias Combine = (macOS 10.15,  tvOS 13, iOS 13, watchOS 6)
availabilityalias Float16 = (tvOS 14, iOS 14, watchOS 7)
availabilityalias temporaryDirectory = (macOS 10.12, tvOS 10, iOS 10, watchOS 3)
availabilityalias sortedKeys = (macOS 10.13, tvOS 11, iOS 11, watchOS 4)
availabilityalias SwiftUI = (macOS 10.15, tvOS 13, iOS 13, watchOS 6)

Actually, it looks like @lorentey already made a draft PR along these lines back in January.

(I see this and @xymus’ PR as complementary to each other, not as alternatives.)

9 Likes

I think this is the most useful approach. It allows us to encode the reason for these availability checks in the name of the alias. I have some checks around iOS 11.3 in some code not because any API changed, but because behavior changed—especially for testing. Being able to write this:

availabilityalias AnimationsHappenImmediately = (iOS 11.3, macCatalyst 10.15)

would make my test code look like this:

if #available(AnimationsHappenImmediately, *) {
    XCTAssertEqual(controller.dimmingView?.alpha, 0)
}
else {
    let animation = try XCTUnwrap(
        controller.dimmingView?.layer.animation(forKey: "opacity")
            as? CABasicAnimation
    )

    XCTAssertEqual(animation.fromValue as? CGFloat, 0)
}

To me, this is much more readable than having platform version numbers strewn all over the place.

2 Likes

As an aside, I believe the availability of DiscontiguousSlice and RangeSet is incorrect.