Episteme and Techne     About     Projects     Archive     Feed

Enums in Swift

Part 1 of a series on Swift enums, pattern matching, and generics.

Parts of this blog post are adapted from a talk I gave at the Swift Language Users Group.

Like many programming languages both ancient and modern, Swift supports enums. However, Swift’s enums are considerably more powerful than those found in some of its predecessors.

Enums are value types, like Swift’s structs, and can be given computed properties and methods. Enums cannot be given stored properties.

Enums can actually be categorized into three distinct subcategories, informally referred to here as basic enums, raw value enums, and associated value enums. Read on to find out about each subcategory.

Basic Enum

The basic enum just enumerates a number of fixed cases:

enum Direction {
    case North
    case South
    case East
    case West
    // For conciseness, could also declare as:
    // case North, South, East, West
}

// To create:
let myDirection = Direction.North

// To test:
if myDirection == Direction.North {
  println("you're facing north")
}

In this form, enums serve the same purpose they do in a language like C or Objective-C: providing a type-safe way to specify exactly one of a finite number of possible values.

This is preferable to, for example, using numbers to implicitly represent cases, in which you have two main problems:

  • What each valid number means is either stored in your head or in a comment somewhere, rather than being baked into the code itself.
  • If someone tries to use an invalid numerical value, the compiler won’t warn you and your code will fail at runtime.

It’s important to note that, unlike C or Objective-C enums, Swift enums declared in the above fashion are not equivalent to integers: North is not an alias for 0, and so forth.

Raw Value Enum

The second form of enum is the raw value enum. Each case of a raw value enum corresponds to a fixed ‘raw’ value:

enum Title : String {
    case CEO = "Chief Executive Officer"
    case CTO = "Chief Technical Officer"
    case CFO = "Chief Financial Officer"
}

Notice the two main differences between the Title enum and the basic Direction enum described previously:

  • The type of the raw value, String, is specified after the name of the enum.
  • Each case must specify an associated raw value.

Raw value enum cases are not aliases of their raw values, and the two cannot be used interchangeably:

// BAD! Will not compile!
let myCEOString : String = Title.CEO
// BAD! Will not compile!
let myCEOValue : Title = "Chief Executive Officer"

However, raw value enums do support conversion between each enum value and its corresponding raw value, via the rawValue property and a special initializer that takes a rawValue argument:

let myString : String = Title.CEO.rawValue
// myString is now "Chief Executive Officer"

let myTitle : Title = Title(rawValue: "Chief Executive Officer")!
// myTitle is now Title.CEO

let badTitle : Title? = Title(rawValue: "not a valid title")
// badTitle is now nil

Note that the initializer that turns a raw value (in this case, a string) into an enum instance returns an optional. This is because it’s possible to invoke the initializer with an invalid raw value, in which case it would return nil instead of an enum value.

Raw value enums whose raw values are of the Int type are treated slightly differently:

enum Planet : Int {
    case Mercury = 1
    case Venus, Earth, Mars // 2, 3, 4
    case Jupiter = 100
    case Saturn, Uranus, Neptune // 101, 102, 13
}

You do not need to explicitly define raw values for every case within an Int-valued raw value enum. Cases without explicit raw values will be assigned values ‘counting up’ from the last explicit raw value, or from 0 if no such explicit raw value exists. In the above example, since Mercury was explicitly assigned 1, Venus is implicitly assigned 2, and Earth is implicitly assigned 3. However, since Jupiter was explicitly assigned 100, Saturn implicitly gets 101, and so forth.

Associated Value Enum

This is where things begin to get interesting. Enums that aren’t raw value enums can be declared in such a way that each case can store associated data. What does that mean? Take the following example, ‘borrowed’ from the official Swift book:

enum Barcode {
  case UPCA(sys: Int, data: Int, check: Int)
  case QRCode(data: String)
}

let myNormalBarcode = Barcode.UPCA(sys: 0, data: 2791701919, check: 3)
let myQRCode = Barcode.QRCode(data: "http://example.com")

The Barcode enum has two cases, UPCA and QRCode. However, each instance of a Barcode, which is one of those two cases, also has additional data associated with it! Exactly what data depends on the case. In the UPCA case, three integers are associated with a UPC-A barcode. In the QRCode case, one string is associated with a QR code.

The associated values need not be labeled. The above example can be more concisely expressed as follows:

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

let myNormalBarcode = Barcode.UPCA(0, 2791701919, 3)
let myQRCode = Barcode.QRCode("http://example.com")

Not all cases in an associated value enum need to contain associated data. For example, Swift’s optionals are implemented as an enum!

enum Optional<T> {
    case None       // i.e. nil
    case Some(T)    // not nil, some value of type T
}

Associated value enums implement what are known as sum types. One way to think of them is to consider them a type which represents one of several possible types (or type combinations):

enum EitherIntOrString {
    case AsInt(Int)
    case AsString(String)
}

EitherIntOrString can be thought of as a type that represents either an Int value or a String value (but not both at the same time). What about a generic version that can be used with any two types?

// Does not compile!
enum Either<T, U> {
    case First(T)
    case Second(U)
}

Unfortunately, this example code does not compile due to compiler limitations, although the code itself is perfectly valid. There are a number of workarounds to achieve the same effect.

Sum types are great at representing data structures like trees. For example, the following example describes an enum which could be used to construct a tree representing a JSON data structure:

enum JSONNode {
    case NullNode
    case StringNode(String)
    case NumberNode(Float)
    case BoolNode(Bool)
    case ArrayNode([JSONNode])
    case ObjectNode([String:JSONNode])
}

// JSON: [10.0, "hello", false]
let myJSONTree : JSONNode = .ArrayNode([.NumberNode(10.0), .StringNode("hello"), .BoolNode(false)])

Now that we’ve created associated value enums, how do we work with them? Unlike the other two subcategories of enums, associated value enums can’t be tested within if or while statements. This is true even for those cases that don’t contain associated values, like Optional’s None case. Instead, we must use pattern-matching via switch:

let myNode : JSONNode = // ...
switch myNode {
    case .NullNode: println("null")
    case let .StringNode(s): 
        // Switch statements allow us to extract the associated value 
        // for an enum case, via let-binding
        println(s)
    case let .NumberNode(n): println("\(n)")
    case let .BoolNode(b): println("\(b)")
    case .ArrayNode: println("Array")
    case .ObjectNode: println("Object")
}

The next blog post in this series will discuss pattern-matching in more detail.