SE-0407: Member Macro Conformances

Hello, Swift community.

The review of SE-0407: Member Macro Conformances begins now and runs through September 4th, 2023.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to me as the review manager via email or the forum messaging feature. When contacting me as the review manager directly, please put "SE-0407" in the subject line.

Trying it out

If you'd like to try this proposal out, you can download a toolchain supporting it for macOS. No feature flag is necessary.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at

https://github.com/apple/swift-evolution/blob/main/process.md

Thank you,

John McCall
Review Manager

7 Likes

I am in favour of this pitch as this makes adding protocol conformances to classes easier. My only doubt is how the member macro conformances can be unit tested? Currently extension macros also facing the same unit testing gap as reported here. Would be nice if this is addressed in the pitch.

I think this proposal fills an important gap in the macro system. Member macros are already being used as a tool to generate witnesses to fulfill a protocol conformance, and I believe the member macro should have access to the same information that the extension macro has when generating the code that states the conformance itself. I also believe that this proposal together with extension macros subsumes the need for witness macros as described by the macro vision document. There are two implications of this approach versus witness macros that I can think of:

  1. A conformance in the original source can be inherited from a super class, written explicitly, or implied by another protocol conformance. Because the macro cannot differentiate inherited and implied conformances from explicitly written ones, it means the programmer is expected to implement the entire protocol conformance if they explicitly state the conformance themselves, e.g.
@attached(member, conformances: Codable)
@attached(extension, conformances: Codable)
macro ImplementCodable = ...

@ImplementCodable
struct S { ... }

extension S: Codable {} // presumably this will prevent '@ImplementCodable' from generating any of the witnesses

In other words, whether or not to generate witnesses is determined at the level of stating the protocol conformance instead of fulfilling each witness individually. I think this is the right level of granularity for macros that can generate conformances because it's straightforward to reason about, and it runs a much lower risk of circularity due to needing to compute semantic information to pass to the macro. By stating the conformances in the attached macro attribute, we can reason about the witnesses that will be generated prior to macro expansion. I fear that needing more semantic information that comes out of conformance checking (including associated type inference!) will lead to either lots of circularity errors, or a very difficult-to-understand conformance checking behavior. If a programmer wants a macro to generate an implementation for a specific protocol witness, that use-case could be satisfied by explicitly writing the function declaration and using a function-body macro.

  1. There must always be an explicit macro attribute to get "conformance synthesis" via macros. IMO, this is a good thing! The attribute serves as a handle for documentation about what exactly the macro generates, and if you're in an IDE, to view the expanded code. And because the macro can also add the extension that states the conformance, writing @Equatable is not more verbose than writing : Equatable.

Yes, having the ability to conditionally generate protocol witnesses inside a type's primary declaration is critical for supporting conformance synthesis via macros.

Yes. I thoroughly considered the alternative solution presented by the proposal, because I have definitely wanted something like @implementation extension in my own Swift code in the past due to the pervasive stylistic preference of implementing protocol conformances inside extensions. It's harder to reason about protocol witnesses when they're scattered about your source file because some of them have to be in the primary declaration while others can be in the extension. HOWEVER, this line of reasoning in the proposal has fully convinced me that @implementaiton extension is not the right direction for Swift:

The primary drawback to this notion of implementation extensions is that it would no longer be possible to look at the primary definition of a type to find its full "shape": its stored properties, designated/required initializers, overridable methods, and so on. Instead, that information could be scattered amongst the original type definition and any implementation extensions, requiring readers to stitch together a view of the whole type. This would have a particularly negative effect on macros that want to reason about the shape of the type, because macros only see a single entity (such as a class or struct definition) and not extensions to that entity.

Something like @implementation extension is very tempting to reach for, but we'd just be trading one category of scattered declarations for another, and the problem this would create for macros is a nonstarter. Keeping storage, overridable methods, etc, in the same declaration is much more crucial than keeping all witnesses for a protocol conformance in the same declaration. For example, it would be extremely difficult to reason about what your type's member-wise initializer looks like if stored properties are scattered about a source file across many different extensions.

I have not used other languages or libraries with a macro system similar to Swift's attached macros.

An in-depth study with lots of fretting over how attractive @implementation extension seemed to be initially.

3 Likes

Writing @Equatable may not be longer than : Equatable, but it complicates the process of implementing Equatable. Each time you want to implement Equatable, you have to decide whether to synthesize it or add the conformance manually. If you switch from a synthesized implementation to a custom one, you'll have to change from using the macro to using the regular protocol inheritance syntax – unless it's recommended to always use the macro, which would split the Swift world into protocols that should always be inherited from via macro.

Some might argue that this is already the case with macros like @Observable. I believe the attached macro spelling makes sense in cases like @Observable because it does more than just provide protocol witnesses; it also modifies the accessors of your properties. However, synthesizing Equatable/Comparable/Codable isn't as drastic. Witness macros, as described in the vision, would allow us to stick with the familiar protocol inheritance clause and enable library authors to use the intuitive behavior of "a type conforming to MyProtocol can synthesize witnesses if all stored properties also conform to MyProtocol." While a protocol inheritance clause might not technically be an explicit macro attribute, it has essentially functioned as one for years with Equatable and similar protocols. It's deeply ingrained in the minds of every Swift user and could serve as an entry point for IDEs to offer viewing the expanded code.

2 Likes

SE-0407 has been accepted.

1 Like