Howard Lovatt
Howard Lovatt

Reputation: 1008

When would you use an enum with associated values in preference to a static factory?

In Swift you can define an enum and give it a property via an associated value, e.g.:

protocol SizeEnum {
    var length : Double? { get } // Length should be >= 0 - has to be an Optional for errors
}

enum SizesEnum : SizeEnum {
    case Short(length : Double) // 0 <= length <= maxShort
    case Long(length : Double) // length > maxShort
    private static let maxShort =  1.0
    var length : Double? {
    get {
        switch self {
        case let .Short(length):
            if length >= 0 && length <= SizesEnum.maxShort { // Need to error check every access
                return length
            }
        case let .Long(length):
            if length > SizesEnum.maxShort { // Need to error check every access
                return length
            }
        }
        return nil // There was an error
    }
    }
}

SizesEnum.Short(length: 0.5).length // [Some 0.5]
SizesEnum.Short(length: 2).length // nil
SizesEnum.Long(length: 2).length // [Some 2.0]
SizesEnum.Long(length: -1).length // nil

However this is not ideal because:

  1. The error checking for the length parameter can only be done on access, you cannot intercept the init
  2. The length parameter is surprisingly long winded

An alternative, which seems better to me, is to use a static factory, e.g.:

protocol SizeStruct {
    var length : Double { get } // Length should be >= 0 - is *not* an Optional
}

struct SizesStruct : SizeStruct {
    static func Short(length : Double) -> SizeStruct? {
        if length >= 0 && length <= maxShort { // Check at creation only
            return SizesStruct(length)
        }
        return nil
    }
    static func Long(length : Double) -> SizeStruct? {
        if length > maxShort { // Check at creation only
            return SizesStruct(length)
        }
        return nil
    }
    let length : Double
    private static let maxShort = 1.0
    private init(_ length : Double) {
        self.length = length
    }
}

SizesStruct.Short(0.5)?.length // [Some 0.5]
SizesStruct.Short(2)?.length // nil
SizesStruct.Long(2)?.length // [Some 2.0]
SizesStruct.Long(-1)?.length // nil

Given that the static factory solution is neater, when would I actually use an enum with values? Am I missing something? Is there a killer use case?

In response to drewag

For Optional other languages, e.g. Java and Scala, you use factories, the Java version is described here: http://docs.oracle.com/javase/8/docs/api/java/util/Optional.html the factory is the of method.

In Swift you would do something like:

class Opt { // Note only Some stores the value, not None
    //class let None = Opt() - class variables not supported in beta 4!
    class Some<T> : Opt {
        let value : T
        init(_ value : T) {
            self.value = value
        }
    }
    private init() {} // Stop any other ways of making an Opt
}

Opt.Some(1).value // 1

This is probably the optimal example for enum since no error checking is required, but even so the factory version is competitive. The Optional example is so straightforward you don't even need a factory, you just create the Somes directly. Note how None doesn't use any storage.

The Barcode example shows how much better the factory technique is; in practice not all collections of 4 Ints are a valid UPCA and not all Strings are a valid QR Code, therefore you need error checking which is painful with enums. Here is the factory version:

class Barcode { // Note seperate storage for each case
    class UPCABarcode : Barcode {
        let type : Int, l : Int, r : Int, check : Int
        private init(type : Int, l : Int, r : Int, check : Int) {
            (self.type, self.l, self.r, self.check) = (type, l, r, check)
        }
    }
    class func UPCA(#type : Int, l : Int, r : Int, check : Int) -> UPCABarcode? {
        if ok(type: type, l: l, r: r, check: check) {
            return UPCABarcode(type: type, l: l, r: r, check: check)
        }
        return nil
    }
    class func QRCode(#s : String) -> Barcode? { // Have not expanded this case; use same pattern as UPCA
        return Barcode()
    }
    private init() {} // Prevent any other types of Barcode
    class func ok(#type : Int, l : Int, r : Int, check : Int) -> Bool {
        return true // In practice has to check supported type, range of L and R, and if check digit is correct
    }
}

Barcode.UPCA(type: 0, l: 1, r: 2, check: 3)

If you use the enum version of Barcode then every time you use a Barcode you have to check its validity because there is nothing to stop invalid barcodes. Whereas the factory version does the checking at creation. Note how Barcode has no storage and UPCA has custom storage. I didn't code QRCode because it uses the same design pattern as UPCA.

My impression is that the enum version looks great in tutorials but soon becomes painful in practice because of error handling.

Upvotes: 3

Views: 2074

Answers (1)

drewag
drewag

Reputation: 94763

I believe the biggest killer use case is Optional. It is built into the language, but an optional is simply an enum:

enum Optional<T> {
    case None
    case Some(T)
}

In this case, a member variable like value would not make sense because in the None case, there is literally no value.

Also, right out of the Swift tour:

enum Barcode {
    case UPCA(Int, Int, Int, Int)
    case QRCode(String)
}

With a struct, there would be a lot of wasted member variables and a confusing interface to model this kind of data.

I don't believe it makes sense to use an associated value for an enum if the same type is used for every case. In that case a member variable is cleaner. The focus of associated values is to more accurately model certain types of data. The most useful cases are ones where different instances of a type can have different data associated with them. This could potentially be done with subclasses but then one would need to downcast to access more specific variables and the declarations would be much more verbose. Enums are a concise way to represent this type of data.

Another example could be web requests:

struct NetRequest {
    enum Method {
        case GET
        case POST(String)
    }

    var URL: String
    var method: Method
}

var getRequest = NetRequest(URL: "http://drewag.me", method: .GET)
var postRequest = NetRequest(URL: "http://drewag.me", method: .POST("{\"username\": \"drewag\"}"))

When I think of "enum" I don't think of "factory" at all. Normally factories are for larger more complex class structures. Enums are supposed to be very small pieces of data with little logic.

Upvotes: 1

Related Questions