Which (breaking) changes would you be willing to accept in a major language release? [+ Feature wishes]

Make enums non-exhaustive when used across module boundaries.

Packages today basically cannot use enums in their public API if there is any possibility that new cases will be added in the future, because those cases will require a (breaking) major version increment. That's really difficult, because you need everybody to explicitly add support for the new major version in unison.

Swift has non-exhaustive enums, which force switch statements to handle @unknown default: patterns and allow libraries to add new cases later. You see that in SDK libraries, but unfortunately, it is not available to source packages.

Enums are one of Swift's best and most loved features. They've been around since it was first shown to the public. Packages are used all the time, by everyone, and they are expected to use semantic versioning to distribute updates (such as bugfixes) without breaking existing code. Unfortunately the two features are effectively incompatible with each other, and this massively limits the libraries we are able to build.

5 Likes

I would feel better if we force the use of fileprivate over private when it’s actually available in the file’s scope. Cases like private extension reuses private keyword but provides fileprivate semantics instead, introducing unnecessary confusion and styling issues.

I’m personally satisfied with fileprivate (or private(file) as someone proposed), and I believe making the semantics clear at first glance is more important than eliminating keywords.

2 Likes

I agree, especially after re-reading Default Protocol Implementation Inheritance Behaviour - The current situation and what/if anything should be done about it :flushed:

Is it there for anything other that Objective-C compatibly and references? I'd like to see both of those split off.

Yes, this would be fantastic.

Related, I would love to see exhaustivity checking possible for structs. I tend to prefer structs over enums for almost everything, but it would be nice if I could tell the compiler somehow that this is an "exhaustive" struct. Perhaps using CaseIterable or something?

2 Likes

What do you mean by "exhaustive" struct? When using in a switch statement? Please show an example.

public struct ServerEnvironment: Equatable, CaseIterable  {
    public static let allCases = [.production, .qa, .dev]

    public static let production = ServerEnvironment(host: "production.example.com", name: "Production", ...)
    public static let qa = ServerEnvironment(host: "qa.example.com", name: "QA", ...)
    public static let dev = ServerEnvironment(host: "dev.example.com", name: "Development", ...)

    private init( ... ) { }
}

// elsewhere:
let environment: ServerEnvironment = ...
switch environment {
    case .production: // do something only for Production
    case .qa: // do something only for QA
    case .dev: // do something only for Development
}

The "exhaustive" bit is that I would not need a default case because I have handled every possible case as defined by the CaseIterable conformance. It is not possible for the user of ServerEnvironment to get anything except one of those three values.

3 Likes

Maybe this will be possible if we get something like @const and then the compiler could infer that from a @const allCases or something along the lines.

4 Likes

yeah the constexpr proposal(s) have been stuck for months now :pensive:

1 Like

To answer your title: ALL OF THEM.

I put Swift's ability to evolve above everything else.

4 Likes

functions have mangled names, which can be used for hash value for example.

You can currently achieve this in a way like:

public struct ConformanceLimiter {
  fileprivate init() {}
}

public protocol ClosedProtocol {
  var conformanceLimiter: ConformanceLimiter { get } // can not be constructed outside of the library
}

It is a bit ugly, but do the deal.

2 Likes

In this case you can implement the conformanceLimiter as a fatalError:

var conformanceLimiter: ConformanceLimiter {
    fatalError("I'm outside the file so I can't make one, but I can do anything else and conform")
}

Alternatively, you can harvest a ConformanceLimiter from one of the library’s own types and return that.

Yeah, I've done this before with an associated type to prevent "copying" a conformance limiter from one conformer to another:

public protocol MyClosedProtocol {
    static var closer: Closer<Self> { get }

    // regular requirements...
}

public struct Closer<T> {
    // publicly visible, but only internally constructible
    // generic type prevents using a Closer<T> on different conformers of "MyClosedProtocol"
    internal init() { }
}
1 Like