A Swift macro that generates functions from closure properties, making closures callable as regular functions while preserving the benefits of struct-based dependency injection.
Using structs instead of protocols for dependencies offers many advantages:
- Static dispatch - Better performance with no protocol witness table overhead
- Generic passing - You can use generic structs like
Dependency<T>, which isn't possible with protocols - Easy mocking - Simple to create test doubles by just setting closure properties
However, declaring functions as closures can hurt readability. Compare these two approaches:
With closures (hard to read):
struct APIClient {
let uploadFile: (_ file: Data, _ fileName: String, _ mimeType: String, _ completion: @escaping (Result<URL, Error>) -> Void) -> Void
let searchUsers: (_ query: String, _ limit: Int, _ offset: Int) async throws -> [User]
}
// Calling closures - unclear what each parameter is
client.uploadFile(fileData, "photo.jpg", "image/jpeg", { result in
// handle result
})
let users = try await client.searchUsers("john", 10, 0) // What do these numbers mean?With generated functions (clean and readable):
struct APIClient {
@CallableAsFunction
let uploadFile: (_ file: Data, _ fileName: String, _ mimeType: String, _ completion: @escaping (Result<URL, Error>) -> Void) -> Void
@CallableAsFunction(visibility: .public)
let searchUsers: (_ query: String, _ limit: Int, _ offset: Int) async throws -> [User]
}
// Clean, readable API with labeled parameters and trailing closure syntax
client.uploadFile(file: fileData, fileName: "photo.jpg", mimeType: "image/jpeg") { result in
// handle result
}
let users = try await client.searchUsers(query: "john", limit: 10, offset: 0) // Much clearer!The closure syntax obscures the function's purpose and makes it harder to understand what parameters are expected.
@CallableAsFunction bridges this gap by generating clean function signatures from your closure properties. You get the best of both worlds: the flexibility and performance of struct-based dependencies with the readability of protocol-based APIs.
Point-Free recommends struct-based dependencies for The Composable Architecture, using structs with closures instead of protocols. @CallableAsFunction is a solution to make that pattern more ergonomic by improving readability without sacrificing the benefits.
Control the visibility of generated functions:
struct Serialization<T> {
@CallableAsFunction // Default: internal
private let encode: (_ value: T) throws -> Data
@CallableAsFunction(visibility: .public)
private let decode: (_ data: Data) throws -> T
@CallableAsFunction(visibility: .inherited) // Inherits from property
public let validate: (_ data: Data) -> Bool
}Optional parameters automatically receive default nil values in the generated function:
struct UserService {
@CallableAsFunction
let findUser: (_ id: Int?) -> User?
}
// Generated function:
// func findUser(id: Int? = nil) -> User? {
// return findUser(id)
// }
// Can be called without arguments:
let user = service.findUser() // Uses default nil
let user = service.findUser(id: 123) // Explicit value- Swift 5.10+
Add the package to your Package.swift:
dependencies: [
.package(url: "https://github.com/MaximKotliar/CallableAsFunctionMacro.git", from: "0.1.0")
]- Parameter names and labels - Automatically extracts and preserves parameter names from closure signatures
- Async functions - Full support for
asyncclosures with properawaithandling - Throwing functions - Handles
throwswith correcttryusage - Async throwing - Combined
async throwssupport - Inout parameters - Properly handles
inoutparameters with&prefix - Optional parameters - Optional parameters automatically get default
nilvalues in the generated function - Complex return types - Supports arrays, optionals, tuples, and other complex types
- Void return types - Omits explicit
-> Voidfor cleaner syntax - Visibility modifiers - Control function visibility with
.private,.internal,.public, or.inherited
All generated functions are marked with @inline(__always) for optimal performance.