- Proposal: SE-NNNN
- Author: Dan Appel
- Status: Awaiting review
- Review manager: TBD
This proposal introduces a new keyword that can be applied to enums which allows new cases to be introduced in extensions.
Swift-evolution thread: [RFC] Extensible Enums
Enums are a powerful feature which provides a lot of benefit if you have a limited number of behaviors. For example, associated values provide the ability to make every case essentially a separate type. However, due to the static nature of enums, they cannot be used in situations where they would otherwise be a perfect fit.
An example of this would be the use of an Error enum like so:
enum FileError: ErrorProtocol {
case fileNotFound(path: String)
case corruptedFile(bytes: [Int8])
}
func readFile() throws { ... }
// elsewhere in the codebase
do {
try readFile()
} catch let error as FileError {
switch error {
case .fileNotFound(let path): // handle error
case .corruptedFile(let bytes): // handle error
}
} catch { ... }
While this is generally a good approach, it can be very dangerous for library consumers if the author exposes the error to the user. This is due to the fact that the switch
statement has to be exhaustive and is only satisfied when all enum cases have been accounted for. What this means for library authors is that every time they add a new case to a public enum, they are breaking the exhaustivity of the switch
and making their library backwards-incompatible.
Currently, the best workaround is to use a struct
with static instances and overloading the ~=
operator. This allows for similar switch
behavior but overall is much less flexible, missing key features such as associated values.
Another example is when the library is split into multiple modules, where the error is defined in the first module and the second module wants to add some error cases. An enum is very rarely used in this case because you cannot add cases in other modules. Instead, library authors either use an error protocol, and add more types that conform to it, or use the struct approach shown above. While this is not terrible, adding cases in extensions would better translate the intention of the author and adds more flexiblity.
The solution proposed is quite simple: add an extensible
keyword/modifier that can be applied to enums, which would require the default
case when switched on and allow new cases to be added in extensions.
Here is the translation of the very first example to the use an extensible
enum instead, with a new case added:
extensible enum ThingError: ErrorProtocol {
case fileNotFound(path: String)
case corruptedFile(bytes: [Int8])
case failedReadingFile
}
func readFile() throws { ... }
// elsewhere in the codebase
do {
try readFile()
} catch let error as ThingError {
switch error {
case .fileNotFound(let path): // handle error
case .corruptedFile(let bytes): // handle error
default: // handle future errors that don't exist yet
}
} catch { ... }
For the second example, we can simply extend the enum in the higher-level module.
// Module FileProtocol
extensible enum FileError: ErrorProtocol {
case fileNotFound(path: String)
}
protocol FileProtocol {
func read() throws
}
// Module File
extension FileError {
case corruptedFile(bytes: [Int8])
case failedReadingFile
}
struct File: FileProtocol {
func read() throws { ... }
}
A new keyword would be added to the language which is only allowed in front of the enum
keyword. When an enum is marked extensible
, new cases can be added in extensions and switches that are performed on it require a default
case.
There is no impact on existing code since this is purely an additive feature.
No alternatives have been considered (yet).