Contents

SE-0546: Same-file memberwise initializer extensions

Summary of changes

Allows a struct's memberwise initializer to be defined in a same-file extension.

Motivation

When a struct's memberwise initializer is meant to be public (or package), it is only possible to define it in its base declaration:

// βœ…
public struct Post {
  public var title: String
  public init(title: String) { self.title = title }
}

Defining it as an extension is currently a compiler error:

public struct Post {
  public var title: String
}
extension Post {
  // πŸ›‘ invalid redeclaration of synthesized memberwise 'init'...
  public init(title: String) { self.title = title }
}

This organizational limitation was [brought up][forum-post] on the forums many years before Swift had macros in the language, but now that structs can be generated by macros, there is no way to publicize their memberwise initializers:

[forum-post]: https://forums.swift.org/t/why-doesn-t-an-init-in-an-extension-suppress-the-synthesized-init/35182

@Draftable public struct Post {
  public var title: String

  // Generated:
  public struct Draft {
    public var title: String?
  }
}

extension Post.Draft {
  public init(title: String?) { self.title = title }  // πŸ›‘
}

Further, it is impossible to conform a public macro-generated type to a protocol with a requirement matching its internal synthesized initializer:

public protocol TitleInitializable {
  init(title: String?)
}

// πŸ›‘ Initializer 'init(title:)' must be declared public...
extension Post.Draft: TitleInitializable {}

Proposed solution

Allow memberwise initializers to be defined in same-file extensions of the struct:

public struct Post {
  public var title: String
}
extension Post {
  // βœ…
  public init(title: String) { self.title = title }
}

Detailed design

A handwritten initializer that is indistinguishable from the synthesized initializer (i.e. it has the same argument names and types defined in the same order) will now suppress the default, synthesized initializer when defined in an unconstrained, same-file extension. This includes both memberwise and default init() initializers.

Source compatibility

The change is additive, and only allows code that previously failed to compile to compile.

ABI compatibility

This change requires no changes to ABI.

Implications on adoption

This feature can be freely adopted and un-adopted in source code with no deployment constraints and without affecting source or ABI compatibility.

Future directions

Future directions for memberwise initializers has been brought up before on the forums, including [a pitch][explicit-memberwise-initializers] for explicit synthesis, as well as [other shorthands][shorthand-syntax]. This pitch shouldn't have any effect on further evolution of memberwise initializer functionality.

[explicit-memberwise-initializers]: https://forums.swift.org/t/explicit-memberwise-initializers/22893 [shorthand-syntax]: https://forums.swift.org/t/a-new-keyword-that-stands-in-for-a-tedious-initializer-of-arbitrary-length/60173/22

Alternatives considered

An alternative would be to allow these extensions to be defined anywhere in the module, but as mentioned in the original forum thread, this would make it more difficult for humans to read and reason about the code. Limiting to the same file minimizes this burden, and there are similar precedents are set for syntesizing Codable and Hashable conformances in same-file extensions.

Another alternative is to leave things as is, and folks can work around the problem with static func create APIs, or rearranging property order just to allow for a differently-ordered version of a memberwise initializer to be publicized. Each of these alternatives is not without its limitations: a static function introduces a naming/design burden on the code, and deliberately reordering properties to allow an initializer to be publicized is fragile.

Acknowledgments

Thanks Diana Ma ("Taylor Swift") for first suggested this functionality on the Swift forums.