Enum inheritance

Pitch: allow enums to inherit cases.

  1. Is this idea sound?
  2. Is this even possible given the current compiler implementation?
  3. Would this be a complex feature to implement for someone who isn't familiar with the compiler's codebase?

Example:

enum NetworkError: Error {
    case timedOut
    case cannotFindHost
    case notConnectedToInternet
}

enum MyAPIError: NetworkError {
    case responseInvalid
    case sessionExpired
    case notEnoughMinerals
    case insufficientVespineGas
}

func didReceiveResponse(JSON: [String:Any], error: MyAPIError) {
    switch error {
        case .notConnectedToInternet: presentNotConnectedAlert()
        case .sessionExpired: logout()

        case .notEnoughMinerals, .insufficientVespineGas:
            playSound(identifiedBy: JSON)

        case .responseInvalid, NetworkError: 
            presentGenericTryAgainLaterAlert()
    }
    // no default case needed because all the cases are covered.
}
2 Likes

If it is somewhat simple, would you be interested in helping out?

In my mind, the primary questions here are:

  1. This only considers inheritance of cases, what happens with all other aspects of the enum (methods, protocol conformances, etc?
  2. Is there a compelling use case where the alternatives (e.g. wrapping as case networkError(NetworkError), or simply duplicating the cases) are particularly bad?
  3. Can you assign a NetworkError to a MyAPIError?
  4. Should this instead be part of a more general consideration of “inheritance for value types”? There have been a lot of requests for similar features for struct, like newtypes or strong type aliases.
3 Likes

Thanks for the thought provoking response.

It'd be good to have those aspects inherited but I suppose you'd run into strange issues, such as an implementation in the superenum unable to provide a return value in a case like this (riffing off the example above):

extension NetworkError {
    var localizedAlertMessage: String {
        switch self {
            // exhaustively match NetworkError cases; compiler is happy by Swift 4 standards.
        } 
    }
}

func alertMessage(for error: MyAPIError) -> String {
    return error.localizedAlertMessage // 🤯🙇🏻💥
}

Perhaps the compiler could detect the inheritance and either require the inheritor to override and call super or require the superenum to provide a default case? But what if the implementation is marked final or it isn't public? Yep, gross!

This situation alone is enough for me to reject the pitch as a very bad idea!

The equivalent switch pyramid of doom (switching within a switch)? I suppose given the complexity it introduces into the compiler and complexity introduced for inheritance of other aspects of the enum, composing an enum and switching on it is potentially easier for the developer to mentally bear than the problems identified above.

I would expect so.

I don't know.

Thanks again for your response! Much appreciated.

@jawbroken's comment has really bought out the inheritance vs composition debate for me. Clearly, this is a case of composition because I really want the cases of the inherited enum and less so its other aspects.

Also, a protocol could be used to define the interfaces common between the two that'd make the localizedAlertMessage example work much more elegantly.

1 Like

This idea has been discussed on this list before. It is a kind of value subtyping, which is a large feature that is not at all simple.

I would encourage you to look through the archives and familiarize yourself with the discussion so far, then summarize it for the list again if you're interested in discussing the idea further, so that what's been said already doesn't have to be rehashed.

For one thing, your conception of the idea has it backwards. In your example, MyAPIError is not a subtype of NetworkError. Rather, NetworkError becomes a subtype of MyAPIError. That is because every NetworkError value is a valid MyAPIError, but not the other way around. Please see previous discussions for more details.

5 Likes

I'd suggest reviewing SE-0192 if you haven't already done so (see the links to the pre-review and review discussions as well).

When you extend variant types like this, the subtype relationship is the opposite of what you might expect. The newly extended type is a supertype of the type it is extending, and as @jawbroken mentions, that raises some interesting questions.

1 Like

It is easy enough to write:

enum MyAPIError {
    case networkError(NetworkError) // Enum to be extended.
    case responseInvalid
    case sessionExpired
    case notEnoughMinerals
    case insufficientVespineGas
}

So I don't think it worth the complication.

@anandabits has written an own manifesto about this topic.

Given this, the switch is not worse than your example:

func didReceiveResponse(JSON: [String:Any], error: MyAPIError) {
    switch error {
        case .notConnectedToInternet: presentNotConnectedAlert()
        case .sessionExpired: logout()

        case .notEnoughMinerals, .insufficientVespineGas:
            playSound(identifiedBy: JSON)

        case .responseInvalid, .networkError: 
            presentGenericTryAgainLaterAlert()
    }
    // no default case needed because all the cases are covered.
}

and if you wanted to be more detailed, it's still quite readable (until you start nesting a few levels deep):

func didReceiveResponse(JSON: [String:Any], error: MyAPIError) {
    switch error {
        case .notConnectedToInternet: presentNotConnectedAlert()
        case .sessionExpired: logout()

        case .notEnoughMinerals, .insufficientVespineGas:
            playSound(identifiedBy: JSON)

        case .responseInvalid, .networkError(.responseInvalid), .networkError(.sessionExpired): 
            presentGenericTryAgainLaterAlert()

        case .networkError(.notEnoughMinerals), .networkError(.insufficientVespineGas):
            goPlayMoreStarCraft()
    }
    // no default case needed because all the cases are covered.
}

and you can assign values with just the case wrapped around it:

let networkError: NetworkError = .sessionExpired
let error: MyAPIError = .networkError(networkError)

Have you looked at RawRepresentable?

Here is an example how it can be extended but I believe you can do the same with subclassing.

Was about to say the same thing as @masters3d . Here's how I use it:

/// Extend this struct with `static let keyName: Key = "domain.key"` to add cases
struct Key: RawRepresentable, ExpressibleByStringLiteral {
    let rawValue: String
    
    init(_ value: String) { self.rawValue = value }
    init?(rawValue: String) { self.init(rawValue) }
    init(stringLiteral value: String) { self.init(value) }
    init(unicodeScalarLiteral value: String) { self.init(value) }
    init(extendedGraphemeClusterLiteral value: String) { self.init(value) }
}

It looks very similar to an enum. Swift will autocomplete when expecting a type of Key and this can be switched on as well.

<Never mind; I should read more carefully before posting ;-)>

1 Like