Reputation: 1352
I'm making a macro like this:
@attached(peer, names: arbitrary)
public macro Lazify(name: String, lock: Protectable) = #externalMacro(module: "MacroModule", type: "LazifyMacro")
It should be used to attach to a function declaration, to generate some other variables.
But I got the error Circular reference expanding peer macros on <function_name>
, the code is below:
class SomeClass {
var _internal_lock: NSLock = .init()
static let classLock: NSLock = .init()
// Error: Circular reference expanding peer macros on 'createLazyVariable()'
@Lazify(name: "internalClassName", lock: SomeClass.classLock)
func createLazyVariable() -> String {
return "__" + NSStringFromClass(type(of: self))
}
}
If I change the lock: Protectable
to lock: String
then it's ok, but it won't have type check when using the macro.
And further if I use a instance property lock _internal_lock
and the error will change to Instance member '_internal_lock' cannot be used on type 'SomeClass'; did you mean to use a value of this type instead?
So I wonder why I can't use the classLock
or the _internal_lcok
here?
If I use a global instance lock then it can compile:
let globalLock: NSLock = .init()
class SomeClass {
@Lazify(name: "internalClassName", lock: globalLock)
func createLazyVariable() -> String {
return "__" + NSStringFromClass(type(of: self))
}
}
Upvotes: 0
Views: 648
Reputation: 270708
This is a circular reference because to resolve the name SomeClass.classLock
, Swift must first expand Lazify
. Why? Because Lazify
can introduce arbitrary
names, so the meaning of SomeClass.classLock
might be different after @Lazify
is expanded. @Lazify
can't affect names outside of its scope, so if you use a name that is declared in a different class, or global let
, it works. I'm not sure about this explanation though - it might as well be a bug, perhaps related to this.
In any case, it seems like Lazify
would add a declaration with the name of whatever string is passed to its name
parameter. This is not a good design, since, at least, it would be difficult to rename this using the "Refactor -> Rename" option in Xcode.
Instead of generating whatever name the user wants, you can use the suffixed
or prefixed
options in @attached(peer, names: ...)
, and always generate a name from the name of the declaration to which the macro is attached. For example, use:
@attached(peer, names: suffixed(_lazify))
// you can write multiple "suffixed(...)" here, if you need
Then,
@Lazify(lock: SomeClass.classLock)
func createLazyVariable() -> String { ... }
should be implemented to generate createLazyVariable_lazify
.
You can't use instance properties here for a similar reason to why something like this doesn't work:
class Foo {
let x = 1
let y = x // "self" is not available here!
}
I think this is checked before the macro expansion, so this is not a problem of your macro, per se.
To allow instance members, you can add an overload like this:
public macro Lazify<T, P: Protectable>(lock: KeyPath<T, P>) = ...
Usage:
@Lazify(lock: \SomeClass._internal_lock)
// in the macro implementation, you would generate something like this:
self[keyPath: \SomeClass._internal_lock]
// to access the lock.
From your previous question, it seems like a better design would be to just use a property wrapper:
@propertyWrapper
struct Lazify<T> {
let lock: Protectable
let initialiser: () -> T
var wrappedValue: T {
mutating get {
if let x = lazy {
return x
}
return lock.around {
if let x = self.lazy {
return x
}
let temp = initialiser()
self.lazy = temp
return temp
}
}
set { lazy = newValue }
}
private(set) var lazy: T?
}
If you really want a macro, you can make Lazify
an accessor
macro as well. Declare the internalClassName
property yourself and pass the code in createLazyVariable
to the macro as a closure.
@attached(peer, names: prefixed(_lazy_))
@attached(accessor, names: named(get), named(set))
public macro Lazify<T>(lock: Protectable, initialiser: () -> T) = ...
@Lazify(lock: SomeClass.classLock) {
return "__" + NSStringFromClass(type(of: self))
}
var internalClassName: String
// expands to
private(set) var _lazy_internalClassName: String? = nil
var internalClassName: String {
get {
if let exsist = self._lazy_internalClassName {
return exsist
}
return SomeClass.classLock.around { [weak self] in
guard let self else {
fatalError()
}
if let exsist = self._lazy_internalClassName {
return exsist
}
let temp = "__" + NSStringFromClass(type(of: self))
self._lazy_internalClassName = temp
return temp
}
}
set { _lazy_internalClassName = newValue }
}
Upvotes: 1