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.
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.
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?
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.
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.
public struct ConformanceLimiter {
fileprivate init() {}
}
public protocol ClosedProtocol {
var conformanceLimiter: ConformanceLimiter { get } // can not be constructed outside of the library
}
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() { }
}