A macro automatically produces code that could be written by hand. It is quite common to write a computed property where we need access to self or instance members. I find it a real limitation of accessor macros that they can move the initializer into a getter, but self access is artificially forbidden.
As for the SwiftUI macro example: I over-enthusiastically wrote it down before I could glance at its expansion. It's true that the init expression is being wrapped in a closure in the current implementation, which prevents my proposal from working for this use case. For a property wrapper this approach is a perfect fit. A macro produces code and is therefore more flexible. Maybe there is an alternative approach, but that's a decision for the SwiftUI team.
We could add an underscore to the name if consensus is that the proposed feature is experimental.
I am not sure if the new API is much of a burden. It's only for accessor role declarations, and opt-in for those who want to grant self access. Everybody can ignore it safely. It does not pollute the rest of the Swift namespace, because there is a bespoke method that parses macro role parameters. A macro role attribute is not really Swift code. initialization and selfAvailable won't be Swift keywords. In fact, the alternative true|false spelling only looks like a boolean, but isn't.
@Lazy was just the simplest example I could come up with. Its use-case is already well understood because we have lazy var properties as a language feature. Other macros may be more useful. How about one that logs access as a side effect? Or one that uses a different backing store, such as a dictionary or UserDefaults. Or something like @Clamped that limits values to a given range...
I can think of two more examples of "effectful" getters:
An LRU cache that returns a value after moving it to the end of some list.
A "spy" test double that records the number of times this property was accessed.
What I have a hard time thinking of is a practical example of a new effectful getter that would most naturally be expressed with an InitializerClauseSyntax defined by the product engineer using the macro. The only example I can think of so far is Lazy…
Any of those macros could be also lazy, or have a lazy variant. In the Alternatives Considered I mentioned that we cannot add a macro to a lazy var. We have to add laziness to the macro implementation. Currently we cannot fully replicate lazy var semantics in a macro. The missing piece is to allow self access.
A common reason for a lazy property is to delay or avoid computationally intensive initialization. Let's say we want to have a @Thumbnailed macro[1]:
Creates a thumbnail of a given image
Caches the thumbnail
Let's use it:
struct Article {
let heroImage: UIImage
// Currently does not compile (`self` not available)
@Thumbnailed var thumbnail = heroImage
}
To avoid creating a thumbnail wastefully, it postpones that work to the first read access. At that moment, it checks if there is an existing thumbnail for that image in the cache. If not, it creates the thumbnail, caches it and returns it[2].
The pattern "lazy evaluation & caching" is quite useful if initialization is non-trivial and if the number of values is reasonably small, for example DateFormatters.
We could use a lazy var with an initializer expression that does the image processing and caching. It's not too bad if the heavy lifting is provided by a free function:
lazy var thumbnail =
cachedThumbnail(for: heroImage)
Disadvantage: there is a mutating get, which may be OK if the enclosing instance can be mutated. If not, a macro could be a great solution. ↩︎
Note: The cache should ideally have reference semantics so we don't need mutating get. ↩︎
Side note: A getter is not the only way to allow access to instance members. A macro could move the initializer expression into an init accessor if it knows the properties where access is needed. Those have to be added to the accesses list of @storageRestrictions.
Initially, the proposed spelling was initialization: lazy|eager. It matched the precedent of the lazy keyword nicely, but did not adequately describe what is actually happening during type-checking. The same reasoning applies to the alternative initialization: deferred|immediate spelling.
I'm a little more interested to hear why lazy|eager or deferred|immediate are no longer on the table. It's "the what not the how" instead of "the how not the what". There is not going to be one right answer here… but I do think communicating this API in terms of what we want to enable gives us a little more freedom in the future to determine how we want to enable that.
Ha! Now it's your turn to come up with practical examples
I admit that I do want elephants on bicycles, but not during property initialization. And I'd like to keep it private and not tell the compiler about it...
Any of those would be fine for me. Maybe delayed would be a bit more clear than deferred because the latter has a second meaning. I'll defer to those who preferred selfAvailable to elaborate on their reasoning.
I tried to be very careful about avoiding source-breaking changes when I decided to add the initialization: selfAvailable parameter to the macro role attribute, which defaults to selfUnavailable. But maybe that's a bit too conservative? I have some concerns that this setting will be largely ignored by the macro community. If that's the case, we end up with a bunch of macros that could allow self access, but don't. Quickly brainstorming three ideas:
Diagnose warning if initialization: parameter is missing
This will nudge macro authors to at least think about it and explicitly declare selfUnavailable if needed, or decide to ignore/suppress the warning.
Compiler setting instead of role attribute parameter ACCESSOR_MACRO_SELF_AVAILABLE_FOR_INITIALIZER=true[1]
This should be the default setting for new macro projects[2]. Old projects can opt in. No fine-grained control for different accessor macros in the same package.
Change default behavior with new Swift version
A macro compiled with Swift 6.4 keeps the existing behavior. When a newer compiler is used, selfAvailable becomes the default, potentially breaking client code in some rare edge cases. With this approach we could argue that selfUnavailable is not an option, and the initialization: ... parameter should be removed from the proposal.
That's a mouth full, bike shedding needed if this idea is chosen. ↩︎
If blissfully ignored by a macro author, and initializer is re-contextualized where self is in fact unavailable: no big deal, it will be diagnosed by the type checker in the new context. ↩︎