Skip to content

MaximKotliar/CallableAsFunctionMacro

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@CallableAsFunction

A Swift macro that generates functions from closure properties, making closures callable as regular functions while preserving the benefits of struct-based dependency injection.

The Problem

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.

The Solution

@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.

Visibility Control

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

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

Requirements

  • Swift 5.10+

Installation

Add the package to your Package.swift:

dependencies: [
    .package(url: "https://github.com/MaximKotliar/CallableAsFunctionMacro.git", from: "0.1.0")
]

Supported Features

  • Parameter names and labels - Automatically extracts and preserves parameter names from closure signatures
  • Async functions - Full support for async closures with proper await handling
  • Throwing functions - Handles throws with correct try usage
  • Async throwing - Combined async throws support
  • Inout parameters - Properly handles inout parameters with & prefix
  • Optional parameters - Optional parameters automatically get default nil values in the generated function
  • Complex return types - Supports arrays, optionals, tuples, and other complex types
  • Void return types - Omits explicit -> Void for cleaner syntax
  • Visibility modifiers - Control function visibility with .private, .internal, .public, or .inherited

All generated functions are marked with @inline(__always) for optimal performance.

About

@CallableAsFunction macro for Swift closures.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages