Skip to main content

Swift Recursive Enumerations

Introduction

Recursive enumerations are a powerful feature in Swift that allows an enumeration to have instances of the same enumeration type as associated values. This creates self-referential data structures, which are essential for representing hierarchical or nested data like arithmetic expressions, linked lists, trees, and more.

Due to the way Swift allocates memory for enumerations, we need a special keyword called indirect to tell the compiler that we're creating a recursive structure. In this tutorial, we'll explore how recursive enumerations work and how they can be used to model complex data structures.

Understanding Recursive Enumerations

The Problem with Recursion in Enumerations

To understand why we need special handling for recursive enumerations, let's consider how Swift allocates memory for enumerations:

  1. Swift tries to allocate enumerations as efficiently as possible
  2. Without associated values, enumerations use only the memory needed to store the different cases
  3. With associated values, Swift calculates the memory needed based on the associated values

If an enumeration tried to include itself as an associated value directly, Swift would face an infinite recursion problem when calculating the memory needed.

The indirect Keyword

Swift solves this problem with the indirect keyword:

  • When you mark an enumeration case as indirect, Swift adds a layer of indirection
  • Instead of storing the value directly, Swift stores a reference to the value
  • This breaks the infinite recursion problem, allowing enumerations to reference themselves

Creating a Recursive Enumeration

Let's start with a basic example - representing arithmetic expressions:

swift
indirect enum ArithmeticExpression {
case number(Int)
case addition(ArithmeticExpression, ArithmeticExpression)
case multiplication(ArithmeticExpression, ArithmeticExpression)
}

In this example:

  • The number case represents a simple number
  • The addition case represents the addition of two expressions
  • The multiplication case represents the multiplication of two expressions

Notice how both addition and multiplication cases have ArithmeticExpression instances as associated values, making this enumeration recursive.

Using the Recursive Enumeration

Let's create some expressions using our recursive enumeration:

swift
// Create the expression (5 + 4) * 2
let five = ArithmeticExpression.number(5)
let four = ArithmeticExpression.number(4)
let sum = ArithmeticExpression.addition(five, four)
let two = ArithmeticExpression.number(2)
let product = ArithmeticExpression.multiplication(sum, two)

Now, let's write a function to evaluate this expression:

swift
func evaluate(_ expression: ArithmeticExpression) -> Int {
switch expression {
case let .number(value):
return value
case let .addition(left, right):
return evaluate(left) + evaluate(right)
case let .multiplication(left, right):
return evaluate(left) * evaluate(right)
}
}

let result = evaluate(product)
print("Result: \(result)") // Output: Result: 18

The evaluate function itself is recursive - when it encounters an expression that contains sub-expressions, it calls itself to evaluate those sub-expressions first.

Marking the Entire Enumeration as Indirect

If most or all cases of your enumeration are recursive, you can mark the entire enumeration as indirect:

swift
indirect enum BinaryTree<T> {
case empty
case node(value: T, left: BinaryTree<T>, right: BinaryTree<T>)
}

This binary tree enumeration can contain itself as associated values for its child nodes.

Example of Using a Binary Tree

Let's create a simple binary tree:

swift
// Create a binary tree
let leaf1 = BinaryTree.node(value: 3, left: .empty, right: .empty)
let leaf2 = BinaryTree.node(value: 5, left: .empty, right: .empty)
let node = BinaryTree.node(value: 4, left: leaf1, right: leaf2)

This creates a tree with root value 4, left child value 3, and right child value 5.

Now let's write a function to count the nodes in a binary tree:

swift
func countNodes<T>(_ tree: BinaryTree<T>) -> Int {
switch tree {
case .empty:
return 0
case let .node(_, left, right):
return 1 + countNodes(left) + countNodes(right)
}
}

let nodeCount = countNodes(node)
print("Number of nodes: \(nodeCount)") // Output: Number of nodes: 3

Real-World Applications of Recursive Enumerations

Recursive enumerations are especially useful for:

  1. Parsing and interpreting code: Creating abstract syntax trees
  2. Working with hierarchical data: File systems, organizational charts
  3. Implementing data structures: Linked lists, trees, graphs
  4. Representing mathematical expressions: As seen in our first example

Linked List Example

Here's how you could implement a linked list using recursive enumerations:

swift
indirect enum LinkedList<T> {
case end
case node(value: T, next: LinkedList<T>)

// Add a value to the end of the list
func append(_ value: T) -> LinkedList<T> {
switch self {
case .end:
return .node(value: value, next: .end)
case let .node(nodeValue, next):
return .node(value: nodeValue, next: next.append(value))
}
}
}

// Create a list
var list = LinkedList<String>.end
.append("Hello")
.append("World")
.append("!")

// Function to print all elements
func printList<T>(_ list: LinkedList<T>) {
switch list {
case .end:
break
case let .node(value, next):
print(value)
printList(next)
}
}

printList(list)
// Output:
// Hello
// World
// !

JSON Parser Example

Let's create a simplified JSON parser using recursive enumerations:

swift
indirect enum JSON {
case string(String)
case number(Double)
case boolean(Bool)
case array([JSON])
case object([String: JSON])
case null

// A simple method to describe the JSON
func describe(indent: String = "") -> String {
switch self {
case .string(let str):
return "String: \"\(str)\""
case .number(let num):
return "Number: \(num)"
case .boolean(let bool):
return "Boolean: \(bool)"
case .null:
return "Null"
case .array(let array):
var result = "Array: [\n"
for item in array {
result += "\(indent) \(item.describe(indent: indent + " "))\n"
}
result += "\(indent)]"
return result
case .object(let dict):
var result = "Object: {\n"
for (key, value) in dict {
result += "\(indent) \"\(key)\": \(value.describe(indent: indent + " "))\n"
}
result += "\(indent)}"
return result
}
}
}

// Create a sample JSON object
let jsonSample = JSON.object([
"name": .string("John"),
"age": .number(30),
"isStudent": .boolean(false),
"hobbies": .array([
.string("Reading"),
.string("Swimming"),
.string("Coding")
]),
"address": .object([
"street": .string("123 Main St"),
"city": .string("Anytown")
])
])

print(jsonSample.describe())
/* Output:
Object: {
"name": String: "John"
"age": Number: 30.0
"isStudent": Boolean: false
"hobbies": Array: [
String: "Reading"
String: "Swimming"
String: "Coding"
]
"address": Object: {
"street": String: "123 Main St"
"city": String: "Anytown"
}
}
*/

Memory Considerations

Recursive enumerations use references behind the scenes, which has some implications:

  1. There's slightly more memory overhead compared to non-recursive enumerations
  2. Reference counting is involved, which can affect performance in tight loops
  3. Very deep recursion can lead to stack overflow errors

For most use cases, these considerations won't matter, but they're good to keep in mind when working with large recursive data structures.

Summary

Recursive enumerations in Swift are a powerful feature that allows you to:

  • Create self-referential data structures
  • Define complex, hierarchical data models
  • Implement classic data structures like linked lists and trees
  • Parse and represent structured data formats

The indirect keyword is essential for recursive enumerations, as it tells Swift to add a level of indirection when storing associated values. This can be applied to individual cases or the entire enumeration.

As you become more comfortable with Swift, recursive enumerations will become an invaluable tool in your programming toolkit, allowing you to express complex relationships and hierarchies with clean, type-safe code.

Exercises

To solidify your understanding of recursive enumerations, try these exercises:

  1. Extend the BinaryTree example to include a method that returns the maximum depth of the tree.
  2. Create a recursive enumeration to represent a file system with files and directories.
  3. Implement a simple calculator that can parse and evaluate arithmetic expressions.
  4. Create a recursive enumeration to represent HTML elements, with tags that can contain other elements.
  5. Extend the JSON parser to include a method that converts the JSON structure to a string representation.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)