Hey all!
My pitch is for libraries that provide macros to allow the consuming end to provide custom configurations/values for the macro to use at compile time.
I have a vague idea of how this could be done and really want to take on the challenge of putting together and documenting a POC. Before I do, though, I wanted to gauge interest here and see if this is even feasible or if maybe this is a lot cooler in my head than it is outside of it (likely).
The Problem
I'm building a macro which would greatly benefit from the ability to accept a custom transform(_:)
-type function. Something that I might receive from an initialization function in a traditional swift package (ie. LibraryName(transform: { $0 * 10 })
)
However, due to the nature of macros, this is not an option. At least not as far as I'm aware.
My proposed solution would look something like this:
Library-Side
Library developers can annotate specific properties:
struct MyMacroStruct {
...
@ConsumerProvided
let transform: TransformClosure = { ... }
...
}
or entire structs:
@ConsumerProvided
struct MyMacroConfig {
let transform: TransformClosure = { ... }
let someVal: String = "..."
}
Consumer-Side
Library consumers can provide these config values which will override the default implementation in the library:
@Providing(for: MyMacroModule)
let transform: TransformClosure = { ... }
or
@Providing(for: MyMacroModule)
let config: MyMacroConfig = .init(
transform: { ... }
)
or even (maybe, spitballing)
#Providing(for: MyMacroModule) {
let transform: TransformClosure = { ... }
let someVal: String = "..."
}
These values must be declared in a file separate from the rest of the project. That is to say, a consumer must not declare these provided values within a file that is compiled normally as part of the greater project. In fact, the file will not be compiled as part of the project at all, and therefore has no access to any part of the project. This means all provided values must be some kind of literal, since they will be taken verbatim and substituted in the library in place of the default value(s). The compiler would be aware of this and display errors if there is a type mismatch or invalid symbol name
Alternative Solution
@freestanding(expression) public macro MyMacro(_ transform: TransformClosure) -> String = #externalMacro(module: "MyLibMacros", type: "MyMacro")
and consumer-side:
extension MyMacro {
let transform: TransformClosure = ...
}
where the extension just provides a default value for the transform
parameter in the macro. Thus, instead of:
let something = #MyMacro({ ... })
we must only say:
let something = #MyMacro
Without getting too deep in the weeds surrounding the how, what do you think? Could you imagine a better solution to the problem? Did I miss something in the docs and all of this was for nothing?