Swiftwater Logo

Enums with Associated Values

This entry is part 4 of 21 in the series Swiftwater

ABSTRACT

Swift enums have a pretty awesome capability. You can associate values with enumerations.

What this means, is that you can actually add some data to an enum value at the time that you assign the enum, and that value can be instance- or context-variable.

In this page, I’ll give a couple of demonstrations of how this can be used.

AN ERROR-HANDLING SYSTEM

In this example, we’ll have a fictional nuclear power plant, administered by one of our favorite homies.

A few things could go wrong, and we want a fairly generic reporting system for them.

In the sample playground, I’ll get a lot fancier, showing how I actually implement localization, but I’ll keep it much simpler, here in the discussion.

What we’ll have, as a general architecture, is a “generic” error enum, with broad classifications of three types of errors, and then three more “specific” enums; each with information about their errors.

The Specific Error Enums

First, we’ll set up the three specific enums, with cases for the individual errors:

enum CoreErrors : String, Swift.Error {
    case header             =   "CORE ERROR!"
    case loTemp             =   "The core temperature is too low."
    case hiTemp             =   "The core temperature is too high."
}

enum GeneratorErrors : String, Swift.Error {
    case header             =   "GENERATOR ERROR!"
    case lowRPM             =   "The generator is running too slowly."
    case highRPM            =   "The generator is running too quickly."
}

enum HomerErrors : String, Swift.Error {
    case header             =   "OPERATOR ERROR!"
    case asleep             =   "Homer is snoring at his post."
    case donutMess          =   "Homer has covered the control panel in jelly."
}

The General Error Enum

Next, we’ll set up the general error enum:

enum ReactorErrors {
    case coreError(_:CoreErrors)
    case generatorError(_:GeneratorErrors)
    case homerError(_:HomerErrors)
    // This is the "secret sauce." This calculated property decodes the current enum value, and extracts the specific string.
    var error: (header: String, details: String) {
        get {
            var containedError = "Unknown Error"
            var header = "Unknown Error"
            
            switch self {
            case let .coreError(specificError) :
                header = CoreErrors.header.rawValue
                containedError = specificError.rawValue
            case let .generatorError(specificError) :
                header = GeneratorErrors.header.rawValue
                containedError = specificError.rawValue
            case let .homerError(specificError) :
                header = HomerErrors.header.rawValue
                containedError = specificError.rawValue
            }
            return (header: header, details: containedError)
        }
    }
}

Note that “error” calculated property. That’s where the action happens.

When you assign an enum case, you also provide a “nested” enum that has a String description of the error.

The cases will extract this error, along with a header, and return the error. This allows you to have a generic handler that is controlled by the source of the error report, as opposed to having to figure it out near the reporting output. In the sample playground, you’ll see how this can be leveraged by a localization token system.

Using the Enums

Finally, we report the errors, and then display the reports:

func printError(_ reactorError: ReactorErrors) {
    print("ERROR: \(reactorError.error.header) -\(reactorError.error.details)")
}

printError(ReactorErrors.coreError(.loTemp))
printError(ReactorErrors.generatorError(.highRPM))
printError(ReactorErrors.homerError(.donutMess))

This will print the following:

ERROR: CORE ERROR! -The core temperature is too low.
ERROR: GENERATOR ERROR! -The generator is running too quickly.
ERROR: OPERATOR ERROR! -Homer has covered the control panel in jelly.

So this allows you to define the error strings in a separate enum declaration (or, if you look at the sample playground, a localized.strings file), and you can deal with specifics in a general way.

NOW, WE GET CRAZY

Remember that in Swift, functions/closures are first-class citizens. They can be treated just like properties and values.

So, how about we put that into action here?

Let’s create another error handling enum that uses closures as its associated values:

enum GenericErrorHandler {
    typealias ErrorHandlerRoutine = ()->Void
    private func _handleError(_ inErrorHandler:ErrorHandlerRoutine) {
        inErrorHandler()
    }
    case systemError(_:ErrorHandlerRoutine)
    case communicationError(_:ErrorHandlerRoutine)
    case userError(_:ErrorHandlerRoutine)
    func handleError() {
        switch self {
        case .systemError(let handler):
            self._handleError(handler)
        case .communicationError(let handler):
            self._handleError(handler)
        case .userError(let handler):
            self._handleError(handler)
        }
    }
}

In the above case, the associated value is defined to be a simple closure.

There is an embedded method: handleError() that is called to execute the appropriate closure. In the sample playground, we get a bit fancier, but we’ll keep it simpler, here.

Now, let’s give the enum its closures:

func displaySystemError() {
    print("Pretend we're displaying an alert, with our particular error, here.")
}

var sysErr = GenericErrorHandler.systemError(displaySystemError)
sysErr.handleError()

sysErr = GenericErrorHandler.systemError({print("This is a very different closure.")})
sysErr.handleError()

let commErr = GenericErrorHandler.communicationError({print("This Could be your code.")})
commErr.handleError()

let userErr = GenericErrorHandler.userError({print("Spank User.")})
userErr.handleError()

Which will output:

Pretend we're displaying an alert, with our particular error, here.
This is a very different closure.
This Could be your code.
Spank User.

Pretty neat, huh? Remember that these closures could be anything. They could be routines that SCRAM the reactor or hit the alarm klaxons (or spank Homer).


SAMPLE PLAYGROUND

/*
 # ENUMS
 
 Here, we'll play with associated value enums.
 
 The scenario is an error reporting system.
 
 We have multiple classes of errors, and, within those classes, specific errors.
 
 We have a localized error string, and a universal error code for each specific error.
 
 This is for a fictional nuclear power plant, run by our favorite plant operator.
 */

// First, we'll set up a Dictionary of localized strings. The key will be the localized string placeholder. This mimics the standard iOS localization pattern.

let localizedStrings:[String:String] = [
// First, we have reactor core errors.
    "ERROR-CORE-HEADER"         :       "CORE ERROR!",
    "ERROR-CORE-LO-TEMP"        :       "The core temperature is too low.",
    "ERROR-CORE-HI-TEMP"        :       "The core temperature is too high.",
// Next, we have generator errors.
    "ERROR-GEN-HEADER"          :       "GENERATOR ERROR!",
    "ERROR-GEN-LO-RPM"          :       "The generator is running too slowly.",
    "ERROR-GEN-HI-RPM"          :       "The generator is running too quickly.",
// Finally, we have operator errors.
    "ERROR-HOMER-HEADER"        :       "OPERATOR ERROR!",
    "ERROR-HOMER-ASLEEP"        :       "Homer is snoring at his post.",
    "ERROR-HOMER-DONUT-MESS"    :       "Homer has covered the control panel in jelly.",
    // This is a general "Error X"
    "ERROR-UNKOWN"              :       "UNKOWN ERROR"
                        ]

// This is a trick to make the NSLocalizedString call easier to deal with. In this case, we'll be indexing the dictionary instead.

extension String {
    var localized: String {
        get { return localizedStrings[self] ?? (localizedStrings["ERROR-UNKOWN"] ?? "LOCALIZATION POOCHED") }
    }
}

// Now, we set up our error enums.

// These are specific errors. We have made them Strings.

enum CoreErrors : String, Swift.Error {
    case header             =   "ERROR-CORE-HEADER"
    case loTemp             =   "ERROR-CORE-LO-TEMP"
    case hiTemp             =   "ERROR-CORE-HI-TEMP"
}

enum GeneratorErrors : String, Swift.Error {
    case header             =   "ERROR-GEN-HEADER"
    case lowRPM             =   "ERROR-GEN-LO-RPM"
    case highRPM            =   "ERROR-GEN-HI-RPM"
}

enum HomerErrors : String, Swift.Error {
    case header             =   "ERROR-HOMER-HEADER"
    case asleep             =   "ERROR-HOMER-ASLEEP"
    case donutMess          =   "ERROR-HOMER-DONUT-MESS"
}

// These are the general categories, and are expressed as Ints.

enum ReactorErrors {
    case coreError(_:CoreErrors)
    case generatorError(_:GeneratorErrors)
    case homerError(_:HomerErrors)
    // This is the "secret sauce." This calculated property decodes the current enum value, extracts the specific string, and also localizes it before returning it.
    var error: (header: String, details: String) {
        get {
            var containedError = "ERROR-UNKOWN"
            var header = "ERROR-UNKOWN"
            
            switch self {
            case let .coreError(specificError) :
                header = CoreErrors.header.rawValue
                containedError = specificError.rawValue
            case let .generatorError(specificError) :
                header = GeneratorErrors.header.rawValue
                containedError = specificError.rawValue
            case let .homerError(specificError) :
                header = HomerErrors.header.rawValue
                containedError = specificError.rawValue
            }
            return (header: header.localized, details: containedError.localized)
        }
    }
}

// Now, let's put these to the test.

// We simply set a variable to the specific type of error
let reactorError1 = ReactorErrors.coreError(.loTemp)
// Then we display the decoded error.
let err1 = "ERROR: \(reactorError1.error.header) -\(reactorError1.error.details)"

let reactorError2 = ReactorErrors.generatorError(.highRPM)

let err2 = "ERROR: \(reactorError2.error.header) -\(reactorError2.error.details)"

let reactorError3 = ReactorErrors.homerError(.donutMess)
let errorTuple = reactorError3.error
let err3 = "ERROR: \(errorTuple.header) -\(errorTuple.details)"

let reactorError4 = ReactorErrors.homerError(.asleep)

// Test for generic errors (ignoring associated values):
if case .coreError = reactorError3 {
    print("ERROR: Generic Core Error! (if - case - ignoring associated value)")
}

if case .generatorError = reactorError3 {
    print("ERROR: Generic Generator Error! (if - case - ignoring associated value)")
}

if case .homerError = reactorError3 {
    print("ERROR: Generic Operator Error! (if - case - ignoring associated value)")
}

// Switch on specific associated values:
switch reactorError3 {
case .homerError(.asleep):
    print("ERROR: Homer is asleep! (switch)")
case .homerError(.donutMess):
    print("ERROR: Homer had a donut mess! (switch)")
default:
    print("ERROR: some other error (\(reactorError4))! (switch)")
}

// Or catch them in if - case:
if case .homerError(.asleep) = reactorError4 {
    print("ERROR: Homer is asleep! (if - case)")
}

if case .homerError(.donutMess) = reactorError4 {
    print("ERROR: Homer had a donut mess! (if - case)")
}

// Now, remember that closures are first-class citizens in Swift, so we can pass in closures.
enum GenericErrorHandler {
    typealias ErrorHandlerRoutine = ()->Void
    
    case systemError(_:ErrorHandlerRoutine)
    case communicationError(_:ErrorHandlerRoutine)
    case userError(_:ErrorHandlerRoutine)
    
    private func _handleSystemError(_ inErrorHandler:ErrorHandlerRoutine) {
        print("THIS IS A SYSTEM ERROR!")
        inErrorHandler()
    }
    
    private func _handleCommError(_ inErrorHandler:ErrorHandlerRoutine) {
        print("THIS IS A COMMUNICATION ERROR!")
        inErrorHandler()
    }
    
    private func _handleUserError(_ inErrorHandler:ErrorHandlerRoutine) {
        print("THIS IS A USER ERROR!")
        inErrorHandler()
    }
    
    func handleError() {
        switch self {
        case .systemError(let handler):
            self._handleSystemError(handler)
        case .communicationError(let handler):
            self._handleCommError(handler)
        case .userError(let handler):
            self._handleUserError(handler)
        }
    }
}

func displaySystemError() {
    print("Pretend we're displaying an alert, with our particular error, here.")
}

var sysErr = GenericErrorHandler.systemError(displaySystemError)
sysErr.handleError()

sysErr = GenericErrorHandler.systemError({print("This is a very different closure.")})
sysErr.handleError()

let commErr = GenericErrorHandler.communicationError({print("This Could be your code.")})
commErr.handleError()

let userErr = GenericErrorHandler.userError({print("Spank User.")})
userErr.handleError()