Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

Extensible Enums

Introduction

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

Motivation

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.

Proposed solution

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 { ... }
}

Detailed design

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.

Impact on existing code

There is no impact on existing code since this is purely an additive feature.

Alternatives considered

No alternatives have been considered (yet).


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.