Skip to content

Commit 17d54cf

Browse files
Update nnnn-protocol-based-attached-macros.md
1 parent 18dd2ad commit 17d54cf

File tree

1 file changed

+144
-24
lines changed

1 file changed

+144
-24
lines changed

proposals/nnnn-protocol-based-attached-macros.md

Lines changed: 144 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,20 @@ protocol Decodable {
108108

109109
When either of these protocols are attached to a given type, the compiler synthesizes the protocol's requirements in the type its attached to. Not only that, but the compiler performs validation of the type to ensure that each of its stored members conform to the protocol which is required for being able to automatically synthesize the conformance. If the requirements can't be automatically synthesized, the requirements can be manually implemented on the type. Not only that but the protocol's requirements can be manually implemented even if the requirements could be synthesized by the compiler.
110110

111-
This kind of functionality can already be somewhat emulated using the 'conformance' field on the `member` or `extension` attached macro type. For example, we could create an attached member macro that conforms the attached type to the `Encodable` protocol and synthesizes the requirements of the protocol:
111+
This kind of functionality can already be somewhat emulated using the 'conformance' field on the `member` or `extension` attached macro type. For example, we could create an attached member macro that conforms the attached type to a given protocol and synthesizes the requirements of the protocol:
112112

113113
```swift
114114
// Declaration
115-
@attached(member, conformances: Encodable, named: named(encode(to:)))
116-
macro EncodableMacro() = #externalMacro(...)
115+
protocol FooBar {
116+
associatedtype Foo
117+
associatedtype Bar
118+
119+
func foo() -> Foo
120+
func bar() -> Bar
121+
}
122+
123+
@attached(member, conformances: FooBar, named: named(foo()), named(bar()))
124+
macro FooBarMacro() = #externalMacro(...)
117125

118126
...
119127

@@ -126,40 +134,150 @@ struct EncodableMacro: ExtensionMacro {
126134
conformingTo protocols: [TypeSyntax],
127135
in context: some MacroExpansionContext
128136
) throws -> [ExtensionDeclSyntax] {
129-
137+
return [
138+
ExtensionDeclSyntax(
139+
...
140+
inheritanceClause:
141+
InheritanceClauseSyntax(inheritedTypes:
142+
InheritedTypeListSyntax(protocols.map {
143+
InheritedTypeSyntax(type: $0)
144+
})
145+
),
146+
...
147+
)
148+
]
130149
}
131150
}
132151
```
133152

134-
This kind of functionality would be incredibly useful for
153+
Although this works fine, it does lack some of the functionality that makes these kinds of protocols what they are today. Specifically, if one directly conforms to a given protocol (e.g. `FooBar`) then the protocol's requirements aren't automatically synthesized like they would have been had the macro been used intstead:
135154

136-
While this attached macro type would certainly be useful for more complex protocols with multiple required members where we wanted to synthesize each of the members, creating a macro for each of the protocol's members would become quite cumbersome and polute the global namespace with a number of different single-use macros. It we were to instead create a single attached macro that we would attach to each of the different declarations we want to synthesize, the implementation of the macro could become exceeding complex to account for all of the different members that it's supposed to generate, assuming that we could even structure a macro in this way.
155+
```swift
156+
struct FooBarImplA: FooBar {
157+
// error: Missing implementations for `foo()` and `bar()`
158+
}
159+
160+
@FooBarMacro
161+
struct FooBarImplB {
137162

138-
Describe the problems that this proposal seeks to address. If the
139-
problem is that some common pattern is currently hard to express, show
140-
how one can currently get a similar effect and describe its
141-
drawbacks. If it's completely new functionality that cannot be
142-
emulated, motivate why this new functionality would help Swift
143-
developers create better Swift code.
163+
}
164+
165+
// Conformance to `FooBar` + requirements synthesized in an extension of `FooBarImplB`:
166+
//
167+
// extension FooBarImplB: FooBar {
168+
// func foo() -> Self.Foo {
169+
// ...
170+
// }
171+
// func bar() -> Self.Bar {
172+
// ...
173+
// }
174+
// }
175+
```
176+
177+
Furthermore, this workaround doesn't allow for the associated protocol to be composed into other protocols or types like the way that `Encodable` and `Decodable` are composed in the `Codable` typealias:
178+
179+
```swift
180+
typealias HashableFooBar = (FooBar & Hashable)
181+
// OR: `protocol HashableFooBar: FooBar, Hashable { }`
182+
183+
struct FooBarImpl: HashableFooBar {
184+
func hash(into hasher: inout Hasher) {
185+
...
186+
}
187+
188+
// error: Missing implementations for `foo()` and `bar()`
189+
}
190+
```
191+
192+
Because of these drawbacks we are proposing a new protocol attached macro type that mirrors the behavior of these kind of special protocols where the macro is invoked by conforming a type to the protocol.
144193

145194
## Proposed solution
146195

147-
Describe your solution to the problem. Provide examples and describe
148-
how they work. Show how your solution is better than current
149-
workarounds: is it cleaner, safer, or more efficient?
196+
The proposed solution is to create a new attached macro type `conformance` whose usage is restricted to protocol declarations:
197+
198+
```swift
199+
@attached(conformance, ...)
200+
protocol FooBar {
201+
...
202+
}
203+
```
204+
205+
The implementation of the macro would then be invoked whenever the protocol is formally adopted on a conrete type, that is to say adopted by a class, struct, enum, or actor. When added to the inheritance clause of another protocol, the macro wouldn't be invoked. However, any conformance to the new protocol on a conrete would invoke the macro for that conrete type. For example:
206+
207+
```swift
208+
@attached(conformance, ...)
209+
protocol Foo {
210+
associatedtype Foo
211+
func foo() -> Foo
212+
}
213+
214+
// `Foo`'s macro isn't invoked here
215+
protocol FooBar: Foo {
216+
associatedtype Bar
217+
func bar() -> Bar
218+
}
150219

151-
This section doesn't have to be comprehensive. Focus on the most
152-
important parts of the proposal and make arguments about why the
153-
proposal is better than the status quo.
220+
// `Foo`'s macro is invoked here and synthesizes the `foo()` requirement.
221+
struct FooBarImpl: FooBar {
222+
func bar() -> Self.Bar {
223+
...
224+
}
225+
}
226+
```
154227

155228
## Detailed design
156229

157-
Describe the design of the solution in detail. If it involves new
158-
syntax in the language, show the additions and changes to the Swift
159-
grammar. If it's a new API, show the full API and its documentation
160-
comments detailing what it does. The detail in this section should be
161-
sufficient for someone who is *not* one of the authors to be able to
162-
reasonably implement the feature.
230+
The new `conformance` attached macro type would accept two different parameters in the attribute's usage:
231+
232+
1) `additionalMembers`
233+
234+
If provided, this field will accept a list of named parameters that specify any additional declarations that the macro will create. All of the declarations in the associated protocol are implicitly included and therefore it's not necessary to include them in this field. For example, if the `Encodable` protocol were declared as this kind of macro it would include an additional declaration for the `CodingKeys` enum that it creates:
235+
236+
```swift
237+
@attached(conformance, additionalMembers: named(CodingKeys), macro: #externalMacro(...))
238+
protocol Encodable {
239+
240+
func encode(to encoder: Encoder) throws
241+
}
242+
```
243+
244+
2) `macro`
245+
246+
This field is required to be include in the attribute and is provided with the implementation specification of the macro:
247+
248+
```swift
249+
@attached(conformance, macro: #externalMacro(...))
250+
protocol FooBar {
251+
...
252+
}
253+
```
254+
255+
Next, a new macro protocol type would be created in the `SwiftSyntax` library for specifying the implementation of the macro:
256+
257+
```swift
258+
public protocol ProtocolConformanceMacro: AttachedMacro {
259+
260+
static func expansion(
261+
of protocol: ProtocolDeclSyntax,
262+
attachedTo declaration: some DeclGroupSyntax,
263+
in context: some MacroExpansionContext
264+
) throws -> [DeclSyntax]
265+
}
266+
```
267+
268+
The parameters of the required expansion function are defined as follows:
269+
270+
- `protocol`:
271+
272+
In lieu of providing an `AttributeSyntax` like is done in all other attached macros, this function accepts the declaration of the protocol that this macro is being invoked.
273+
274+
- `declaration`:
275+
276+
The `declaration` field is provided with the concrete declaration type that the protocol is attached to. The AST this declaration would be equivalent to the declaration that would be provided to the invocation of a `member` or `extension` macro. That is to say that the full AST of the conforming type would be provided, not just the type of the declaration or a "skeleton" of the declaration.
277+
278+
- `context`:
279+
280+
The standard macro expansion context included in all other macro expansion functions.
163281

164282
## Source compatibility
165283

@@ -296,6 +414,8 @@ problem for many adopters of `@inlinable`.
296414

297415
## Alternatives considered
298416

417+
While this attached macro type would certainly be useful for more complex protocols with multiple required members where we wanted to synthesize each of the members, creating a macro for each of the protocol's members would become quite cumbersome and polute the global namespace with a number of different single-use macros. It we were to instead create a single attached macro that we would attach to each of the different declarations we want to synthesize, the implementation of the macro could become exceeding complex to account for all of the different members that it's supposed to generate, assuming that we could even structure a macro in this way.
418+
299419
Describe alternative approaches to addressing the same problem.
300420
This is an important part of most proposal documents. Reviewers
301421
are often familiar with other approaches prior to review and may

0 commit comments

Comments
 (0)