Enums with Associated Values

Pro-tip, do not delete your hard work. I sadly had to rewrite this page from scratch : /

Derivation

Sometimes you will want to pass along information along with your cases (I will be using case and states interchangeably for this entry). This entails associating values with individual instances of our states. OCaml programmers rejoice–this will be familiar to you. Maybe something like this will ring a bell:

type MixedType = | Int of int | String of string

For those that are lost, I'll try to build out a data type to help explain.

struct MixedType {
    enum MType {
        calet origin: position = (0, 0, 0)
let mySphere = Shape.Sphere(origin, 1)
// mySphere: Shape = A sphere centered at the origin, with radius 1
let myPrism = Shape.Prism(origin, origin, 5, 4, 3)
// myPrism: Shape = A prism centered at the origin, with no rotation,
// with dimensions 5x4x3.se int
        case string
    }
    var mType: MType
    var value: Any
}

Using such a type would look something like this:

var myMixedTypes: [MixedType] = [
    MixedType(mType: .int, value: 3),
    MixedType(mType: .string, value: "Hello"),
]

func explainMyType(_ mixedType: MixedType) {
    switch mixedType.mType {
        case .int:
            print("I am an integer: \(mixedType.value as! Int)")
        case .string:
            print("I am text: \(mixedType.value as! String)")
    }
}

for i in myMixedTypes {
    explainMyType(i)
}
// "I am an integer: 3"
// "I am an integer: Hello"

Side note: For those that have not read the section on casting yet, the as! operator converts our value of type any to the type that follows. This as! is basically saying I guarantee that this object is actually of this type, so treat it that way.

We have successfully stored two different types in a list! Of course, this isn't literally true because we combined two into one, but we functionally did. This is a pretty weird example that you won't really see outside of functional programming (I think). But what is important to take away is that we are associating some value with some state.

Declaration and Construction

The method we used above with a wrapper struct works, but is dangerous because of the same issues that led us to using enums, the user can input nonsensical data because we don't enforce our preconditions. For example, one could do something like:

let myBrokenType = MixedType(mType: .int, value: I am not an int")

The way to get around this is with enums with associated values. We do this with the syntax

enum <Type Name> {
    case nonassociatedCase
    case associatedCase(<Associated Type>)
    ...
}

Okay, let's put it to work. I am going to create an enum to describe 3D shapes. I'm going to use something called typealias, which allows me to shorthand things. For example, position and orientation are just going to be a 3D tuple of floats, and I don't want to write that (because I'm lazy), so I'm just going to rename them.

typealias Position = (Float, Float, Float)
typealias Orientation = (Float, Float, Float)

enum Shape {
    case Sphere(Position, Float) // position, radius
    case Cube(Position, Orientation, Float) // position, orientation, side-length
    case Prism(Position, Orientation, Float, Float, Float) // position, orientation,
    // width, height, depth
    case Tetrahedron(Position, Orientation, Float) // position, orientation, side-length
}

To create an instance of Shape, we simply use the case as a constructor:

let origin: Position = (0, 0, 0)
let mySphere = Shape.Sphere(origin, 1)
// mySphere: Shape = A sphere centered at the origin, with radius 1
let myPrism = Shape.Prism(origin, origin, 5, 4, 3)
// myPrism: Shape = A prism centered at the origin, with no rotation,
// with dimensions 5x4x3.

Great! But one issue you may find, especially when stacking multiple associations as shown above is that these associations are hard to keep track of. Going back to the declaration is not always going to be easy, and you may not always have comments to say what's what. To fix this, we can add argument names!

enum VerboseShape {
    case Sphere(position: Position, radius: Float)
    case Cube(position: Position, orientation: Orientation, sideLength: Float)
    case Prism(
    position: Position,
    orientation: Orientation,
    width: Float,
    height: Float,
    depth: Float)
    case Tetrahedron(position: Position, orientation: orientation, Float)
}

Now, when instantiating, we must include the parameter names:

let origin: position = (0, 0, 0)
let mySphere = VerboseShapepe.Sphere(position: origin, radius: 1)
// mySphere: VerboseShape = A sphere centered at the origin, with radius 1
let myPrism = VerboseShape.Prism(
    position: origin,
    orientation: origin,
    width: 5,
    height: 4,
    depth: 3
)
// myPrism: VerboseShape = A prism centered at the origin, with no rotation,
// with dimensions 5x4x3.

I personally do not like how much extra code this adds, but I do think that the user needs a description of what they're inputting. For this, we use the wildcard (_) to make our variables anonymous. To be specific, it's not the same level of anonymity that we saw originally where there were literally no names. Here, when you are writing your code, the user will see variable names. But, any reader who comes back to look later, will need to do a tiny tiny bit of fishing to see what this is. So, this is a tradeoff that you will need to consider when making your design decisions.

enum VerboseShape {
    case Sphere(_ position: Position, _ radius: Float)
    case Cube(_ position: Position, _ orientation: Orientation, _ sideLength: Float)
    case Prism(_ position: Position, _ orientation: Orientation, _ width: Float, _ height: Float, _ depth:  Float)
    case Tetrahedron(_ position: Position, _ orientation: orientation, _ sideLength: Float)
}

let origin: Position = (0, 0, 0)
let mySphere = Shape.Sphere(origin, 1)
// mySphere: Shape = A sphere centered at the origin, with radius 1
let myPrism = Shape.Prism(origin, origin, 5, 4, 3)
// myPrism: Shape = A prism centered at the origin, with no rotation,
// with dimensions 5x4x3.

Usage

Let's create a function to print out the properties of a shape. To do this, we are going to need to distinguish when a certain shape is a sphere, cube, or tetrahedron. Before we could compare through equality. Something like:

if shape == .Sphere {
} else if shape == .Cube {
} ...

But by adding cases with associated values we lose our enum's conformance to Equatable. This means that we lose access to ==. To be fair, how does a compiler know what we want for equality? Should .Sphere(_) == .Sphere (all spheres are equal) or should it be equal only if they have the same associations? It is easy to do the latter, we can conform our enum to Equatable: enum Shape: Equatable {...}. But, this will not work for us. We need to check the case, not for equality of fields.

As before, this can be accomplished with both if statements (kind of) and switch statements. Except, instead of if statements, we are going to use something called if-case-let. If you have read about optionals, think if-let and guard let–if it's the right case, assign it to an identifier. If you haven't read about optionals yet, no fear this will just be a tiny bit weird. Here's what an if-case-let looks like:

func describe(_ shape: Shape) {
    if case let .Sphere(position, radius) = shape {
        print("Position: \(position), Radius: \(radius)")
    } else if case .Cube(let position, let orientation, let sideLength) = shape {
        print("Position: \(position), Orientation: \(orientation), Length: \(sideLength)")
    } ...
}

You'll notice we actually have two syntaxes that both work. I don't know of any differences other than keeping the let expressions within the associations has more characters ¯_(ツ)_/¯.

Okay, so what's going on here?

First, we are checking if shape is a case of Sphere. If it is, then we bind it's associated values, in order, to position and radius. If not, we continue until it hits the right case, and assign the properties accordingly.

Switch statements can do the same!

func describe(_ shape: Shape) {
    switch shape {
        case let .Sphere(position, radius):
            print("Position: \(position), Radius: \(radius)")
        case .Cube(let position, let orientation, let sideLength):
            print("Position: \(position), Orientation: \(orientation), Length: \(sideLength)")
    }
}

Combining Patterns

In some cases (😉), you may be repeating code. For example, let's create a function to return position of every shape:

func getPosition(_ shape: Shape) -> Position {
    switch shape {
        case let .Sphere(position, _): return position
        case let .Cube(position, _, _): return position
        case let Prism(position, _, _, _, _): return position
        case let .Tetrahedron(position, _, _): return position
    }
}

Note: The wildcards (_) here mean that there's a value that we don't care about, so we are going to bind them to nothing.

There's potential for a lot of repeated code here... In some cases, you could refactor some of the work into a function. But, even better, you can actually combine cases! This works if you can meet 3 conditions:

  1. All cases bind the same number of variables

  2. All cases bind the same types of variables

  3. Every case binds the same variable names to each type

If these criteria are met you can combine cases with commas:

func getPosition(_ shape: Shape) -> Position {
    switch shape {
        case let .Sphere(position, _), 
        let .Cube(position, _, _),
        let Prism(position, _, _, _, _),
        let .Tetrahedron(position, _, _):
         return position
    }
}

Last updated