Unlocking Swift’s Potential: Empowering Declarations with Peer Macros

VINODH KUMAR
4 min readFeb 24, 2024

--

Photo by Desola Lanre-Ologun on Unsplash

Swift, with its versatility and power, offers developers various tools to enhance productivity and streamline code development. Among these tools, Peer Macros stand out as a potent mechanism for extending the functionality of declarations within the same scope. In this article, we’ll explore the concept of Peer Macros and demonstrate their application through a practical example.

Understanding Peer Macros

Peer Macros are a type of macro in Swift that, when applied to a declaration, generate new declarations within the same scope as the original one. Unlike attached macros, which modify the declaration they’re attached to, Peer Macros create additional declarations that are closely related to the original one. These macros are particularly useful for enhancing code organization and maintainability by allowing developers to add methods, properties, or other related declarations without the need for separate extension blocks or external modifications.

Example Scenario

Let’s consider a scenario where we want to enhance a function declaration by automatically adding a completion handler. We can achieve this using a Peer Macro named`AddCompletionHandler. This macro will generate a new method within the same scope as the original function, which includes the completion handler logic.

Macro Definition

To define the AddCompletionHandler macro, we use the peer attribute, indicating that it generates declarations in the same scope. Here’s how the macro is defined:

@attached(peer, names: overloaded)
public macro AddCompletionHandler() = #externalMacro(module: "MacroExamplesImplementation", type: "AddCompletionHandlerMacro")

Macro Implementation

The AddCompletionHandlerMacro type implements the logic for generating the completion handler method. It inspects the original function declaration, extracts necessary information, and generates the new method with the completion handler logic. Here’s the implementation of the macro:

import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros

public struct AddCompletionHandlerMacro: PeerMacro {
public static func expansion(
of node: SwiftSyntax.AttributeSyntax,
providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol,
in context: some SwiftSyntaxMacros.MacroExpansionContext
) throws -> [SwiftSyntax.DeclSyntax] {
guard
let functionDecl = declaration.as(FunctionDeclSyntax.self) else {
// TODO: throw an error here
return []
}

guard
let functionSignature = functionDecl.signature.as(FunctionSignatureSyntax.self),
functionSignature.parameterClause.parameters.count == 0,
let _ = functionSignature.effectSpecifiers?.as(FunctionEffectSpecifiersSyntax.self)?.asyncSpecifier,
let returnTypeSyntax = functionSignature.returnClause?.as(ReturnClauseSyntax.self)?.type.as(IdentifierTypeSyntax.self)?.name.text else {
// TODO: throw an error here
return []
}

let functionName = functionDecl.name.text

print(functionDecl.attributes)

return [DeclSyntax.init(stringLiteral: """
func \(functionName)(onCompletion: @escaping (\(returnTypeSyntax)) async -> Void) {
Task.detached {
await onCompletion(await \(functionName)())
}
}
""")]
}
}

In this implementation,

Macro Struct Declaration:
We define a struct named AddCompletionHandlerMacro that conforms to the `PeerMacro` protocol. This indicates that this macro is responsible for generating peer declarations, i.e., declarations within the same scope as the original declaration.

Expansion Function:
The expansion function is where the macro’s logic is implemented. It takes three parameters:

  1. node: Represents the attribute syntax node associated with the macro expansion.
  2. declaration: Represents the original declaration to which this macro is attached, and from which peer declarations will be generated.
  3. context: Provides contextual information about the macro expansion process.

Guard Statements:
These guard statements ensure that the provided declaration is indeed a function declaration. If the declaration does not conform to `FunctionDeclSyntax`, the function returns an empty array, indicating that no peer declarations will be generated.

Function Signature and Return Type Extraction:
Here, we extract the function signature and return type information from the function declaration. We ensure that the function has no parameters, is marked as async, and extract the return type’s name.

Function Name Extraction:
We extract the name of the function from the function declaration.

Generating New Declaration:
Finally, we generate a new function declaration with the provided completion handler. The generated function uses Task to execute asynchronously. The new declaration is returned as an array of DeclSyntax objects.

Example Usage

Now, let’s see how we can use the AddCompletionHandler macro to enhance a function declaration:

@AddCompletionHandler
func fetchData() async -> String {
return "Hello world!"
}

When we apply the AddCompletionHandler macro to the fetchData function, it automatically generates a new method within the same scope. Here’s the expanded code:

func fetchData(onCompletion: @escaping (String) async -> Void) {
Task {
await onCompletion(await fetchData())
}
}

Conclusion

In conclusion, Peer Macros represent a valuable tool in the Swift developer’s arsenal, offering a seamless way to extend the functionality of declarations within the same scope. By automating the generation of related declarations, Peer Macros streamline code organization, improve maintainability, and boost productivity. With a clear understanding of how to define and implement Peer Macros, developers can leverage this powerful feature to unlock Swift’s potential for building robust and scalable applications.

--

--

VINODH KUMAR

📱 Senior iOS Developer | Swift Enthusiast | Tech Blogger 🖥️ Connect with me on linkedin.com/in/vinodhkumar-govindaraj-838a85100