The phrase “meta” is definitely a Greek time period that’s generally used as a prefix as we speak. It means “past” or “after.” Metaprogramming refers to going “past” simply writing code that runs. It’s not in regards to the app logic itself — like performing a community request, growing UI, or constructing enterprise logic — however in regards to the highly effective follow of writing code that may generate, analyze, or rework one other piece of code. That is the place the Swift language turns into a instrument to govern your codebase.
Why must you care? That is the last word weapon in opposition to boilerplate code. The repetitive code you copy-paste for equality conformance, JSON decoding, or take a look at mocks is a significant supply of bugs and is tough to keep up. Right here, metaprogramming acts as a knight in shining armor, enabling you to write down a single piece of code that generates different repetitive code, making certain consistency from a single supply of reality. It additionally powers the creation of expressive, human-readable Area-Particular Languages (DSLs).
This text explores two runtime metaprogramming strategies in Swift: reflection utilizing Mirror, and dynamic property entry utilizing @dynamicMemberLookup.
The Magic Mirror: Runtime Reflection with Mirror
In commonplace programming, you write code that operates on information. You’re conscious of the variables, their sorts, properties, and strategies throughout compilation. However what if you wish to see it whereas your code is working? What if you wish to write a generic inspector that may study any object? Whether or not it’s a struct Person, an enum NetworkError, or perhaps a class you haven’t carried out but.
This can be a frequent characteristic additionally out there in languages apart from Swift. It’s known as Reflection. It refers to a program’s capacity to examine its construction akin to sorts, relationships, and properties at runtime. In Swift, the first instrument to successfully leverage this functionality is Mirror.
What’s Reflection?
Reflection is a form of metaprogramming that occurs solely at runtime. In contrast to compilation instruments that validate code earlier than execution, reflection examines your app’s objects in reminiscence whereas they’re energetic.
You may consider it as holding a mirror as much as your code. Normally, a operate solely sees the values it’s handed. With reflection, you possibly can see the construction of these values and reply questions akin to:
- What sort of factor are you? (A struct? A category? A tuple?)
- What are the names of your properties?
- What values are at present saved in these properties?
Swift deliberately limits reflection capabilities to protect efficiency and sort security. In contrast to Goal-C, Swift reflection doesn’t permit methodology invocation, mutation, or dynamic kind creation at runtime. Nonetheless, Mirror presents a standardized, secure solution to peek inside cases while you actually want that dynamic conduct.
Tips on how to Use Mirror?
Utilizing a Mirror is sort of easy. You create a Mirror to replicate any occasion you wish to examine.
Contemplate the next code:
struct Person {
let title: String
let age: Int
}
let michael = Person(title: "Michael Scott", age: 44)
// Create the mirror
let mirror = Mirror(reflecting: michael)
After getting the mirror object, one in every of its most helpful properties is youngsters. That is the gathering of all seen components of the mirrored topic. Every youngster is a tuple containing an non-obligatory label (the property title) and a worth (the saved information).
Iterating over the youngsters assortment:
print("Inspecting (mirror.subjectType):")
for youngster in mirror.youngsters {
let propertyName = youngster.label ?? "unknown"
print(" - (propertyName): (youngster.worth)")
}
// Output:
// Inspecting Person:
// - title: Michael Scott
// - age: 44
You may also establish the kind of an object. It’s an non-obligatory enum that may be .struct, .class, .enum, .tuple, .non-obligatory, .assortment, and extra, through the use of the displayStyle property on the Mirror object. You will need to pay attention to the kind you’re coping with when dealing with values. For instance, you would possibly wish to format a .class otherwise than a .tuple in a logging instrument.
Sensible Use Case: Constructing a Generic prettyPrint
One of the simplest ways to make use of Mirror is to create one thing helpful with it. A standard difficulty in debugging is printing advanced objects, leading to unreadable, jumbled output. You should utilize Mirror to write down a generic operate that makes use of reflection to recursively print any object with clear indentation.
This operate doesn’t want prior information of the categories it would print. It’s going to use Mirror to determine it out dynamically.
Check out the operate under:
func prettyPrint(_ worth: Any, indent: Int = 0) {
let mirror = Mirror(reflecting: worth)
// Base case: If the worth has no youngsters, simply print it immediately.
if mirror.youngsters.isEmpty {
print(worth)
return
}
// Decide if it is a assortment to make use of [] as an alternative of ().
let isCollection = mirror.displayStyle == .assortment
|| mirror.displayStyle == .set
|| mirror.displayStyle == .dictionary
let open = isCollection ? "[" : "("
let close = isCollection ? "]" : ")"
// Print kind title (if not a set) and opening bracket.
if !isCollection {
print("(mirror.subjectType)", terminator: "")
}
print(open)
let childIndent = String(repeating: " ", rely: indent + 1)
for youngster in mirror.youngsters {
// At all times print indentation first
print(childIndent, terminator: "")
// If it has a label (like struct properties), print it.
// Arrays often do not have labels for his or her components.
if let label = youngster.label {
print("(label): ", terminator: "")
}
// Recurse for the worth
prettyPrint(youngster.worth, indent: indent + 1)
}
// Print closing bracket with guardian's indentation.
let footerIndent = String(repeating: " ", rely: indent)
print("(footerIndent)(shut)")
}
Now, you possibly can move something to this operate:
struct Firm {
let boss: Person
let staff: [User]
}
let dunderMifflin = Firm(
boss: Person(title: "Michael", age: 44),
staff: [
User(name: "Jim", age: 33),
User(name: "Dwight", age: 38)
]
)
prettyPrint(dunderMifflin)
The prettyPrint operate implements recursion and makes use of Mirror. It’s going to traverse deeply into the Firm struct, find the boss property, confirm that it’s of kind Person, and proceed digging.
It yields the next output:
Firm(
boss: Person(
title: Michael
age: 44
)
staff: [
User(
name: Jim
age: 33
)
User(
name: Dwight
age: 38
)
]
)
It’s a robust, versatile implementation constructed solely on runtime introspection.
