[Pitch] Conformance Builders

Conformance Builders

Introduction

Swift currently provides two micro features: function builders and property wrappers. I propose that we introduce conformance builders for synthesizing protocol conformances on structs. Unlike other proposals, this proposal focuses solely on structs and doesn't try to address static reflection.

Motivation

Implementation Complexity of Compiler Synthesis

The compiler synthesizes implementations for standard-library protocols, such as Hashable and Codable. However, this introduces significant implementation complexity for a very narrow feature, that is not offered to libraries. This makes altering the synthesis behavior difficult for non-compiler developers.

Synthesis for Library Authors

Synthesis is also useful for libraries, which often resort to less performant approaches, such as reflection, for auto-synthesis-like behavior on their protocols.

Furthermore, auto-synthesis from reflection restricts type safety and results in less informative diagnostics, only available at runtime. When, for example, Hashable synthesis is unavailable, the user is notified at compile-time and doesn't need to run their program to verify its validity.

Lack of Synthesis for Regular Users

Libraries that utilize reflection usually rely on custom reflection code β€” not the built-in Mirror API β€” to minimize the performance penalties of reflection and to access more powerful APIs. This is not an option for small-scale libraries or regular users who need auto-synthesis, leading to boilerplate.

Boilerplate, of course, is prone to errors, pollutes the call-site, hinders code maintainability, and can, accidentally, lead to less performant implementations.

Proposed Design

I propose that we introduce conformance builders, as a well-defined, static, and type-safe feature for declaring explicit and implicit synthesis for protocol conformances.

Declaring a conformance builder requires a type marked @conformanceBuilder(of:) with the protocol targeted for synthesis specified in the attribute. This type must provide an init(root:properties) initializer, which is used for passing an instance of the synthesized type and key paths to its properties. Additionally, the conformance builder type can satisfy slightly altered requirements of the target protocol for which it offers synthesized implementations.

As for the application of conformance builders, a custom attribute is offered, which can be used to either declare them as implicit or explicit. For implicit application, the conformance builder custom attribute is applied to the protocol to be auto-synthesized. For explicit synthesis, the custom attribute is applied to the conformance declaration of a struct.

Implicit conformance builders can be overridden by explicit ones, and all synthesized requirements, provided by conformance builders, can be overridden by the conforming type itself.

Here's an example using SwiftUI's View and DynamicProperty protocols.


@conformanceBuilder(of: View)
struct ViewConformanceBuilder<Root> { ... }

@ViewConformanceBuilder
protocol View {
  // Some hidden requirements...

  static var _dynamicProperties(
    instance: Self
  ) -> [KeyPath<Self, DynamicProperty>]

  associatedtype Body: View 
  @ViewBuilder var body: Body { get }
}

struct ContentView: View {
  var body: some View {
    Text("Hello world!")
  }
}

Detailed Design

Semantics

Conformance-Builder Type

The @conformanceBuilder(of:) attribute only accepts protocols in its parameter and can be applied to both structs and enums.

Furthermore, a conformance builder is required to:

  1. satisfy at least one "adjusted" requirement of the protocol it builds conformances to (as described below);
  2. provide one or more initializers of the form init(root:properties:), with the access level of the conformance builder, and a variadic properties parameter (or multiple parameters after properties, or a combination of the two) where all accept values are of type AnyKeyPath (and any of its subclasses).

Adjusted Requirement

Adjusted requirements are all the non-type requirements taken from the protocol the conformance-builder targets, where Self is substituted by the branch builder type, whose generic parameters are inferred as described below.

Implicit Conformance-Builder Requirements

Conformance builders must be unambiguous. This means, that their generic parameters must either be fixed β€” via a same-type constraint or by being specified in the custom attribute β€” or used in their significant initializers. Additionally, synthesis of protocol composition is not currently allowed.

Rationale: Implicitly synthesized protocols provide no means for the user to disambiguate by specifying the conformance builder's generic parameter, through its custom attribute; hence, it is required to be unambiguous.

Application

Applying two or more same-protocol conformance builders a declaration is prohibited. Furthermore, the conformance-builder custom attribute can only be applied to protocols, to indicate implicit synthesis, and to struct conformance declarations, for explicit synthesis.

Type Inference

Type inference may be used to determine: the generic parameters of the significant initializers; or the generic parameters of the conformance-builder type.

To determine the generic parameters of the initializer, the compiler will start by taking the conformance-builder type with any attributes specified on the custom attribute; then, it will call the initializer of this type. The initializer will be passed self in its root parameter and the stored property key paths for properties and the following parameters. After that, the compiler will use the regular type-inference procedure for function calls to determine the generic parameters of the initializer.

To determine the concrete conformance-builder type, the compiler will follow the above process with a pseudo Self instance, and use the inferred and concrete conformance-builder type for requirements with Self in a covariant position, and for static requirements.

Synthesizing Witnesses

In conformance synthesis, the compiler will only synthesize the protocol requirements that the conformance builder has satisfied in their adjusted form, and omit any requirements witnessed by the conforming-struct. In particular, the synthesized members will, first, be transformed to a recursive form, and then replaced so as to transform any Self or self references and parameters.

Example:

@conformanceBuilder(of: Equatable)
struct VectorEquatableBuilder<Root, E1: Equatable, E2: Equatable> { ... }

struct Vector: @VectorEquatableBuilder Equatable {
  let x, y: Double
}

// β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” Transforms to ⬇️ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” \\

struct Vector: Equatable {
  let x, y: Double

  static func == (lhs: Self, rhs: Self) {
    lhs == rhs // The parameters 'lhs' and 'rhs' are of type 'Self'. 
  }
}

// β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” Transforms to ⬇️ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” \\
struct Vector: Equatable {
  let x, y: Double

  static func == (lhs: Self, rhs: Self) {
    VectorEquatableBuilder(root: lhs, properties: \Self.x, \Self.y) ==
    VectorEquatableBuilder(root: rhs, properties: \Self.x, \Self.y) 
  }
}

Diagnostics

Too Many or Too Little Properties

Some conformance builders may require a minimum or a maximum number of parameters. This is communicated to the compiler by the significant initializers, that β€” through overloads or variadic parameters β€” offer a certain number of available properties for their generic constraints. The compiler will understand and diagnose that with an appropriate error:

@conformanceBuilder(of: Equatable)
struct VectorEquatableBuilder<Root, E1: Equatable, E2: Equatable> { ... }

struct Vector3D: @VectorEquatableBuilder Equatable {
  // ❌ 'VectorEquatableBuilder' can't synthesize  
  // 'Equatable' conformance for struct with 3 properties. 

  let x, y, z: Double 
}

Non-satisfied Constraints of Property Key Paths

There are, also, cases wherein the compiler can't find an overload for the generic constraints imposed on the conformance builder. In these cases, the compiler will inform the user about these constraints through notes, and emit an error at the conformance declaration:

struct MyNumber {
  ... 
}

struct Vector: @VectorEquatableBuilder Equatable {
  // ❌ 'VectorEquatableBuilder' can't synthesize
  // 'Equatable' conformance. 

  let x: MyNumber // ℹ️ 'MyNumber' does not conform to 'Equatable'
  let y: MyNumber // ℹ️ 'MyNumber' does not conform to 'Equatable'            
}

Alternatives Considered

Ban Synthesis for Structs containing Class or Unsafe Types

Structs whose properties may cause problems for the synthesized implementations can opt-out by offering their own implementations. Furthermore, some users may utilize immutable classes that do not require special implementations. As for the case of unsafe code, users utilizing it should be aware of its effects, and cautious when using synthesized implementations; nevertheless, unsafe code is often wrapped in safe container types to avoid such problems.

Future Directions

Options in Conformance Builder Attributes

Currently, conformance builders can only alter the synthesis behavior of a given conformance builder through their generic parameters; this approach is quite limited. To address this, we could ease the restriction that mandates that the initializer receive just one property, and use any parameters before root as option parameters β€” similar to arguments on property-wrapper custom attributes.

@conformanceBuilder(of: Identifiable) 
struct TargetedID<Root, ID> {
  let id: KeyPath<Root, ID>, root: Root // No properties are allowed here.

  var id: ID { root[keyPath: id] }
}

struct User: @TargetedID(id: \.userID) Identifiable {
  let name: String
  let userID: UUID
}

Stateful Conformance Builders

Protocols are not allowed to declare stored properties. However, with stateful conformance builders, we could alleviate some boilerplate associated with specifying stored properties by hand:

@conformanceBuilder(of: Identifiable, attributes: stateful) 
struct UUIDIdentifiable<Root: Identifiable> where Root.ID == UUID {
  let root: Root, let id = UUID()
}

struct User: @UUIDIdentifiable Identifiable {
  let name: String
}

// β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” Transforms to ⬇️ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” \\

struct User: Identifiable {
  let name: String
	
  // The conformance builder is stored.
  let __Identifiable_proxy: UUIDIdentifiable<Self> = UUIDIdentifiable(root: self)

  var id: UUID {
    __Identifiable_proxy.id
  }
}

Mutable Root

The proposed solution doesn't offer a way to satisfy mutating requirements, as root can't automatically mutate self, due to conformance builders' value semantics. This can be quite limiting and certain situations, and could be addressed by introducing mutable conformance builders:

@conformanceBuilder(of: IteratorProtocol, attributes: mutable)
struct ForwardedIterator<
	Root: IteratorProtocol, 
	Iterator: IteratorProtocol
> where Iterator.Element == Root.Element {
	var root: Root
	let properties: WritableKeyPath<Root, Iterator>

	mutating func next() -> Root.Element? {
		root[keyPath: properties].next()
  }
}

struct IteratorWrapper<Iterator: IteratorProtocol>: @ForwardedIterator IteratorProtocol {
	var _iterator: Iterator
}

// β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” Transforms to ⬇️ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” \\

struct IteratorWrapper<Iterator: IteratorProtocol>: @ForwardedIterator IteratorProtocol {
  var _iterator: Iterator

  mutating func next() -> Root.Element? {
    let proxy = ForwardedIterator(root: self, properties: \Self._iterator)
    defer { self = proxy.root }

    return proxy.next()
  }
}

Conformance Builders for Enumerations

Enums, like structs, can conform to protocols, and mainstream protocols, such as Codable, synthesize enum conformances. This could be expanded to conformance builder, by first introducing a standard-library union type, such as Either:

enum Either<First, Second> {
  case first(First), second(Second)
}

With an Either type, we can allow conformance builders to declare an init(root:cases:) initializer to indicate support for enum conformance-building:

extension Either: Encodable where 
  First: Encodable, Second: Encodable { ... }

@conformanceBuilder(of: Encodable)
struct EncodableBuilder<Root> {
  private let encodable: Encodable

  init<A: Encodable>(root: Root, cases case: A) {
    encodable = case
  }

  init<A: Encodable, B: Encodable>(root: Root, cases: Either<A, B>) {
    encodable = cases
  }

  func encode(to encoder: Encoder) throws {
    encodable.encode(to: encoder)
  }
}

enum Input: @EncodableBuilder Encodable {
  case number(Double), text(String)
}

// β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” Transforms to ⬇️ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” \\

enum Input: Encodable {
  case number(Double), text(String)

  func encode(to encoder: Encoder) throws {
    let either: Either<Double, String>

    switch either {
      case let .number(value): either = .first(value)
      case let .text(value): either = .second(value)
    }

    return EncodableBuilder(root: self, cases: either).encode(to: encoder)
  }
}

Some design concerns will, first, need to be resolved. Firstly, it is unclear whether struct- and enum-builders should be mandated to be declared as different types. Secondly, it is unclear whether Either is the best solution for our problems, which could perhaps be better solved by variadic generics. Nonetheless, enum conformance-building seems like a straightforward extension to this feature.

Your thoughts

I am really interested to hear what you think of this proposal! All feedback is welcome.

5 Likes

Could you explain what the main differences are between this approach and Automatic Requirement Satisfaction in plain Swift using a Structural protocol? Safe to say that the original authors of that proposal will not be pushing for that design due to Google Brain's decision to deprioritize that work but I am curious if any of their ideas can be pulled into you current pitch since from the surface seems to address the same problem.

In addition I would love to see something about the problem of Scoped Conformances in the future directions.

Im having trouble understanding this. MyEnum does not have payload but it transforms to Input enum with payloads? Maybe I am missing original Input enum?

Good point! I am aware of that proposal, but as one author noted "[the proposal] aims to be an equivalent of static or compile-time reflection" in a post of the pitch thread. Instead, this proposal aims to tackle a very specific case: conformance synthesis for structs. The other proposal also supports enums and takes a very different approach altogether.

ABI Impact

I will add a section about ABI impact and API resilience, but first I want to answer your question.

The proposal you linked to is based on a Structural protocol that enables auto-synthesis of an AST-like data structure. As a result, ABI is impacted by conforming to Structural and structuralRepresentation can be overridden β€” which isn't always desirable.

On the contrary, this proposal offers a compile-time feature, really similar to result builders; hence, it has no ABI impact or way to override the type structure passed to the macro.

Diagnostics

A major criticism of the Structural proposal is the handling of diagnostics. Namely, The AST-like structure is quite "opaque". That is, the compiler can't really understand what property maps to which StructuralProperty after the construction of structuralRepresentation, leading to very limited diagnostics.

This proposal, however, is very similar to result builders, in terms of how properties and components, respectively, are passed into the micro features. Function builders have evolved dramatically from the developmental @_functionBuilder attribute and now provide substantial diagnostics. For example, we get relevant diagnostics when a component does not conform to a type or when the builder can't accept more parameters.

Likewise, result builders will hook into similar mechanisms for generating targeted diagnostics at compile-time, which can be informative and indicate directly which members prevent auto-synthesis β€” as is currently done for synthesized conformances.

Representation

StructuralCons is constructed with the name of a property, its value, and boolean indicating its mutability. This isn't very expressive, in my opinion, not to mention that Swift has KeyPath, which is apt for representing properties.

Furthermore, with a key path returning a String path, as discussed here, Encodable, and perhaps Decodable with a future addition, could be implemented with conformance builders.

Ergonomics

This proposal directly targets conformance synthesis and tries to simplify it as much as possible. Ergo, implicit conformances are a major design aspect of the proposal. Lastly, there is the custom conformance-builder attribute, which allows for flexibility in terms of the synthesis strategy (this is very useful for custom serialization, etc.).


This was a typo, thanks for pointing it out!. The non-transformed enum is supposed to be:

enum Inputs: @EncodableBuilder Encodable {
  case number(Double), text(String)
}

I think scoped conformances would be an incredibly useful future, if β€” of course β€” the implementation can be worked out, but I don't see how that relates to the pitched feature. Could you elaborate?

Ah got it. The automatic requirement satisfaction is a runtime feature vs conformance builders which is a compile time feature. For scoped conformances I was thinking this proposal might be related but now I see they are not.

Thanks for the detailed explanation.

1 Like

What'd the performance implications for change be? So for example, will stack usage grow for each property checked?

The main thing this pitch gets right is using method overloading instead of types to express structural concepts ("more like result builders"). However, solving these use-cases one by one can lead to significant complexity in the language, as opposed to implementing a single mechanism for the more general concept of static structural reflection (which I mentioned in this thread).

Here are some thoughts about what a more general solution might look like:

Being able to provide custom overrides to this type of functionality is very important in Swift, functionality designed using this type of mechanism should compose well with types using custom storage (for instance, Capnproto types, which have semantic stored properties but could customize the layout of those fields). Additionally, it is important that types know when changing their structural representation might affect their usage (otherwise even something as simple as changing the order of properties may have unintended consequences).

struct Vector: @VectorEquatableBuilder Equatable

We don't currently have attributes on conformances, and I'm not sure that we need them to achieve this goal. VectorEquatable can simply refine Equatable and use structural reflection to achieve the desired affect:

/// This doesn't need any new semantics, just conformance to a special protocol
public protocol VectorEquatable: Equatable, Structural {
  public static func == (lhs: Self, rhs: Self) {
    // Not necessary the best API, just conveying the general idea
    @EquatableConformanceBuilder var comparator: EquatableConformanceBuilder<Self>.Comparator
    // Hopefully, this can be optimized by the compiler
    return comparator.components.allSatisfy { $0(lhs, rhs) }
  }
}

@structuralBuilder
public struct EquatableConformanceBuilder<U: Structural> {
  public struct Component {
    /// defined as a closure for simplicity, an ideal implementation should use a form that can be properly optimized
    let compare: (U, U) -> Bool
  }
  public static func buildProperty<T: Equatable>(_ keyPath: KeyPath<U, T>) -> Component {
     Component { lhs, rhs in lhs[keyPath: keyPath] == rhs[keyPath: keyPtah] }
  }
  public struct Comparator {
     let components: [Component]
  }
  public static func buildBlock(_ components: Component...) -> Comparator {
     return Comparator(components: components)
  }
}

So you would end up with something like:

struct Vector: VectorEquatable {
  let x, y: Double

  /// == conformance is synthesized from `VectorEquatable`
}

This would also synthesize the correct conformance for Vector3D, as opposed to what you mentioned in your proposal:

struct Vector3D: @VectorEquatableBuilder Equatable {
  // ❌ 'VectorEquatableBuilder' can't synthesize  
  // 'Equatable' conformance for struct with 3 members. 

  let x, y, z: Double 
}

In the case that a property doesn't match a method overload in the builder, you would get an error similar to what we have now for string interpolation.

1 Like

We do as of SE-0302

I might be missing something, are you referring to @_marker? That is an attribute on a protocol, not a specific conformance to that protocol.

@unchecked SE-0302 (second review): Sendable and sendable closures - #79 by masters3d

Ah right, missed that, thanks!

Sorry for the delayed response.

I'm not sure if you're referring to compilerer or compiled-code performance.

Compiler performance should be similar to that of function builders, as it uses methods to pass a type's properties into the conformance builder to synthesize conformances. I'll keep an eye on that as the feature progresses, but I don't think it will be a problem for now.

As for compiled-code performance, key-paths have very little (if any) performance impact on optimized builds. I made this simple package to test the performance of my VectorEquatableBuilder against custom and synthesized conformances. They all have the same execution time of 125ns on release builds. On some other not-so-reliable, preliminary benchmarks I found that key-paths on debug builds have a performance penalty of about 41%.

I agree that adopting narrowly scoped features is undesirable and unsustainable for Swift. However, static reflection is, in my opinion, too abstract to address in one proposal. Not to mention that, to my knowledge, there aren't very good solutions for problems like informative diagnostics, which β€” I think we can agree β€” are integral to conformance synthesis, and genuinely useful type safety.

Couldn't property wrappers be used to alter the "structural representation", or did I miss something?

As @masters3d pointed out, we do, and I think they offer a pretty lightweight syntax for customizing conformance synthesis.

I'm not in favor of a special, compiler protocol for this type of functionality. For one, protocols impact ABI which is a major setback of Structural, and undesirable for a feature targeted as a macro feature to be widely used. To be fair, we could make Structural a marker protocol and then offer a standard-library:

func structuralRepresentation<T: Structural>(of type: T.Type) -> ...

function. However, that seems limited and still gives the compiler a hard time associating properties with errors.

In the above example, I don't understand why you would need Structural. All the heavy-lifting seems to be up to EquatableConformanceBuilder. Did I miss something?

Could you comment on your choice of @structuralBuilder? I don't have strong feelings about the current name, but this attribute doesn't seem to offer much either in terms of expressiveness.

I really like your idea of including buildProperty and buildBlock! I just wonder how EquatableConformanceBuilder would access an instance of type U (is it through the @EquatableConformanceBuilder custom attribute?).

When thinking about this feature, I struggled with how something like function builders' buildExpression (or buildProperty in your example), and buildBlock could be implemented. But now, based on your example, I thought of another, more intricate, model for conformance builders:

// The target-protocol is inferred by the conformance
// proxy returned by 'buildBlock'.
@conformanceBuilder
enum EquatableConformanceBuilder {
  // Of course, the name can be anything.
  struct PropertyRepresentation<Root> {
    let equalityCheck: (Root, Root) -> Bool
  }

  // We provide 'Root' for customizability.
  static func buildProperty<Root, E: Equtable>(
    _ keyPath: KeyPath<Root, E>,
    of _: Root
  ) -> PropertyRepresentation<Root> { 
    ...
  } 

  static func conformanceProxy<Root>(
    for root: Root,
     _ properties: PropertyRepresentation<Root>...
  ) -> Proxy<Root> {
    Proxy(root: root, properties: properties)
  }
}

extension EquatableConformanceBuilder {
  @conformanceProxy(of: Equatable)
  struct Proxy<Root> {
    let root: Root, properties: [EquatableConformanceBuilder.PropertyRepresentation<Root>]
 
    static func == (lhs: Self, rhs: Self) -> Bool { ... }
  }
}

An example:

struct Vector: @EquatableConformanceBuilder Equatable {
  let x, y: Double
}

// β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” Transforms to ⬇️ β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” \\

struct Vector: Equatable {
  let x, y: Double

  static func == (lhs: Self, rhs: Self) -> Bool {
    let lhs_proxy = EquatableConformanceBuilder.conformanceProxy(
      for: self,
      EquatableConformanceBuilder.buildProperty(\Self.x, of: self),
      EquatableConformanceBuilder.buildProperty(\Self.y, of: self),
    )

    let rhs_proxy = EquatableConformanceBuilder.conformanceProxy(
      for: self,
      EquatableConformanceBuilder.buildProperty(\Self.x, of: self),
      EquatableConformanceBuilder.buildProperty(\Self.y, of: self),
    )

    return lhs_proxy == rhs_proxy
  }
}
1 Like

Sorry, I missed that this was added. Curious, given I was one of the folks that brought up the rationale for it :grinning_face_with_smiling_eyes:. I would mention that @unchecked doesn't affect synthesized code, it just indicates that you know what you are doing to suppress an error.

How do you mean? The scenario I was referring to is something like a packed struct. For instance something with the following API:

struct CustomLayout {
  let x: Int8
  let y: Int16
  let z: Int8
}

but a memory layout of 8 bits of x, immediately followed by 16 bits of y, immediately followed by 8 bits of z, resulting in a 32 bit value. I'm pretty sure the Swift compiler would add padding in this case due to memory alignment, but this type should be representable in Swift and be able to participate in structural reflection. I brought up Capnproto because this will be common in binary serialization formats like Capnp and Protobuf. This makes me think that a Structural protocol and a customization point for modifying its behavior is the right balance. On the other hand, I'm realizing that allowing arbitrary customization of this behavior could make it very difficult to optimize the resulting code.

I basically picked it out of a hat. The only thing I wanted to convey was that it was for more than just building conformances.

This bit of the system still feels very awkward and special-cased to me, as it basically gives us another flavor of extension but with new semantics and a different self. If all you need is the builder's product, we could just allow you to get at that with a statement (like my unfortunately-spelt @EquatableConformanceBuilder var comparator: EquatableConformanceBuilder<Self>.Comparator). This would also allow the same semantics to be used outside of synthesizing a conformance (for example, at the top level in a main.swift).

1 Like

I firmly support synthesis customizability so long as it is the control of the API author. I'm wary of clients modifying the structuralRepresentation requirement without a clear indication of unsafety. (For example, @unchecked indicates unsafety for certain Sendable conformances.)

I think I better understand your concern now.

I thought about incorporating conformance builders with extensions quite a lot and I think that the goal is to combine Swift's nominal type-system constraints with structural constraints. For example, in the following code, we want our Root type to have an unspecified number (structural constraint) of Equatable-conforming ("nominal" constraint) properties:

@structureProjector 
enum EquatableStructure<Root> {
  static func buildProperty<E: Equatable>(
    root: Root, 
    keyPath: KeyPath<Root, E>
  ) -> AnyEquatable { ... }

  static func structureProxy(
    properties: AnyEquatable...
  ) -> [AnyEquatable] { ... }
}

Such constraints do not currently exist in Swift, but they would allow well-defined, type-safe static reflection and synthesis:

// The compiler offers this method:
extension EquatableStructure where Root: EquatableStructure {
  /// Returns the structure proxy for the given 'Root' instance.
  static func proxy(of root: Root) {
    // The compiler knows the structure of 'Root' and uses this structure
    // projector's methods to create and return a structure proxy.
  }
}

extension Equatable where Self: EquatableStructure { 
  //                ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  // This is a structural constraint on 'Self'.

  static func == (lhs: Self, rhs: Self) -> Bool { 
    EquatableStructure.proxy(of: lhs) == 
    EquatableStructure.proxy(of: rhs)
  }
}

In the Vector example, the compiler would create a proxy in EquatableStructure<Vector>.proxy(of:):

struct Vector { let x, y: Double }

extension EquatableStructure where Root == Vector {
  static func proxy(of root: Vector) {
    structureProxy(
      buildProperty(root: root, \.x),
      buildProperty(root: root, \.y)
    )
  }
}

That would be possible since a type's satisfying a structural constraint entails adequate access level and the compiler's being fully aware of a given type's layout.

Now, to declare our synthesized conformance, we'll need to tell the compiler what structure conformance to check for:

extension Vector: @EquatableStructure Equatable {}

I hope that made sense. I am currently working on a second pitch that incorporates some of your feedback and these new "structural" constraints, where I will explain the new behavior in further detail.

Terms of Service

Privacy Policy

Cookie Policy