Skip to content

Instantly share code, notes, and snippets.

@anandabits
Last active April 10, 2019 02:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save anandabits/eb398649a3930d865b35ef435f73c834 to your computer and use it in GitHub Desktop.
Save anandabits/eb398649a3930d865b35ef435f73c834 to your computer and use it in GitHub Desktop.
Explicit Memberwise Initializers

Explicit Memberwise Initialization

Introduction

This proposal provides syntax that allows us to explicitly declare the existing implicit memberwise initializer. Once we have explicit (but concise) declaration syntax in hand we are able to modify it in useful ways when we need more control over the synthesized code.

Swift-evolution thread: Discussion thread topic for that proposal

Motivation

SE-0242 added default parameters to the implicit memberwise initializer for structs. This is a wonderful improvement, but even with this step forward the memberwise initializer remains relatively narrow in its scope of applicability. Any need to deviate at all from the standard behavior faces the steep cliff of having to write every detail out manually. It also loses the ability to elide initial value side effects of any var property that the initializer will provide a value for.

This is unfortunate and unnecessary. Instead, there should be a gradual change in syntactic weight that is commensurate with the required change in behavior. While reading the examples below consider how quickly the boilerplate grows for types with many stored properties. Eliminating this tedium is really important: it obscures the useful information contained in the code that describes the difference from basic (and boring) memberwise initialization. This code that describes the difference should be front and center for a reader.

Proposed solution

The proposed design introduces and builds upon stored property packs. A property pack is an ordered set of properties. When a property pack is expanded by a memberwise initializer, the properties in the pack are spliced into the parameter list and the body of the initializer is prefixed with memberwise intiailization of those properties.

  • Note: the name “property pack” was inspired by parameter packs in variadic generics.

Declaring the implicit memberwise initializer

struct Point {
  let x, y: Double

  // this initializer:
  init(internal...) {}

  // expands to:
  init(x: Double, y: Double) {
    self.x = x
    self.y = y
  }
}
struct Foo {
  private let details: Int
  let exposed: String

  // this initializer:
  private init(private...) {}

  // expands to:
  init(details: Int, exposed: String) {
    self.details = details
    self.exposed = exposed
  }
}

Implicit stored property packs are provided for each access level providing a lower bound of visibility for properties included in the pack. Properties placed in the pack in declaration order. This means that the existing memberwise initializer is spelled by expanding the pack matching the lowest access level used by a stored property of the type. In order to match the synthesized initializer, the explicitly written initializer must also use that access level.

Implicit property packs are based on access levels in order to make it very clear in the initializer declaration the degree to which pack expansion may expose implementation details. This design also supports the intended use case of providing memberwise initialization for more visible properties while using a different initialization strategy for private implementation details.

Mixing memberwise and manual initialization

One example of a stored property that adopts a different initialization strategy is a cache. A cache may be initialized with a default value or an initial value computed using other members stored in the type. In either case today we are required to write an entirely manual initializer.

struct Foo {
  private var cache: [String: Int] = [:]
  let exposed1: String = "hello"
  let exposed2: String = "world"
  
  // this initializer:
  init(internal...) {}

  // note: default arguments are provided as per SE-0242
  // expands to:
  init(exposed1: String = "hello", exposed2: String = "world") {
    // note: initial value assignment of cache is not suppressed so `cache = [:]`
    self.exposed1 = exposed1
    self.exposed2 = exposed2
  }
}

struct Bar {
  private var cache: [String: Int]
  let exposed1: String
  let exposed2: String
  
  // this initializer:
  init(internal...) {
    cache = someComputation(exposed1, exposed2)
  }

  // expands to:
  init(exposed1: String, exposed2: String) {
    self.exposed1 = exposed1
    self.exposed2 = exposed2
    cache = someComputation(exposed1, exposed2)
  }
}

Increasing and lowering visibility

Now that we are able to state the memberwise initializer explicitly we are also able to raise the visibility of an initializer that expands a pack.

struct Foo {
  private let details: Int
  let exposed: String

  // this initializer:
  init(private...) {}

  // visible internally despite exposing initialization of a private property
  // expands to:
  init(details: Int, exposed: String) {
    self.details = details
    self.expose = exposed
  }
}

We are also able to use the access level of setters for var properties instead of getters (the implicit memberwise initializer uses getter visibility).

struct Foo {
  var internalProp: Int
  private(set) var privateProp: Int = 42

  // this initializer:
  init(internal(set)...) {}
  
  // privateProp is omitted because its setter is private
  // and the pack specified `internal(set)` as the minimum setter visibility
  // expands to:
  init(internalProp: Int) {
    // note: initial value assignment of privateProp is not suppressed so `privateProp = 42`
    self.internalProp = internalProp
  }
}

public memberwise initializers are now possible as well.

public struct Foo {
  private let details: Int
  public let exposed: String

  // this initializer:
  public init(private...) {}

  // visible internally despite exposing initialization of a private property
  // expands to:
  public init(details: Int, exposed: String) {
    self.details = details
    self.exposed = exposed
  }
}

Modifing parameter order and external parameter labels

Implicit single-property packs are also provided for each stored property. This can be used to modify the external labels and order of properties in the initializer. Library authors in particular will find this valuable as it provides some sugar while providing explicit control over the exact signature of a public API.

public struct Foo {
  private let details: Int = 42
  public let exposed: String = "hello"

  // this provides an explicit order that is *different* from
  // the property declaration order and modifies the label for details
  // this initializer:
  public init(exposed..., labelForDetails details...) {}

  // expands to:
  public init(exposed: String = "hello", labelForDetails details: Int = 42) {
    self.exposed = exposed
    self.details = details
  }
}

Property packs may be composed before expansion. Pack composition ensures all of the expanded parameters appear in the order of declaration of the expanded properties.

struct Foo {
  var exposed1: Int
  private var detail1: Int = 0
  var exposed2: Int
  private var detail2: Int = 42
  var exposed3: Int
  private var detail3: Int

  // this initializer:
  init((internal | externalLabel detail2)...)
  
  // note: the parameters are in property declaration order
  // expands to:
  init(exposed1: Int, exposed2: Int, externalLabel detail2: Int, exposed3: Int) {
    self.details3 = details3
    self.exposed1 = exposed1
    self.exposed2 = exposed3
    self.exposed3 = exposed3
  }
}

If we had declared the above example without pack expansion and instead just placed detail2... before or after internal... in the parameter list detail2 would appear first or last respectively in the parameter list. Instead, with pack composition, it appears in property declaration order as desired.

This supports use cases where property declaration order is the "source of truth" and packs are expanded in several different memberwise initializers. Without pack composition the parameter order for each initializer would need to be maintained manually and kept in sync with all of the other initializers and the property declaration order.

Note: the use of | for pack composition is tentative. I am open to suggestions.

let properties

By default, a let property is omitted from all parameter packs if its declaration assigns a value. When the assigned value is intended to be a default while still allowing other values during initailization, the @initializable attribute may be used.

struct Foo {
  let timeout = 42
  @initializable let name: String = "first"

  // this initializer:
  init(internal...) {}

  // expands to:
  init(name: String = "first") {
    self.name = name
  }
}

Classes

Designated intializers of a class may use property packs in all the same ways as struct initializers, however because pack expansion immedatiely assigns to properties only packs of properties declared by the class itself are supported. Properties declared by superclasses are not supported. The body of the initializer is responsibly for forwarding any necessary parameters to the superclass initializer.

This proposal does not introduce an implicit memberwise initializers for classes. If experience with explicit memberwise initialization demonstrates that this would be beneficial it can easily be added later.

Detailed design

The crucial design decision in this proposal is to adopt the approach of packs and expansions.

Packs and expansions

Property packs share with variadic generics the notion of a "pack" and an "expansion". When it is applicable, the approach of packs and expansions provides an extremely concise and declarative approach to static metaprogramming. It is simpler and less error prone than more powerful approaches such as hygienic macros. Even better, when expansion alone is not enough these approaches can work in combination: one can imagine a macro using static reflection to analyze a type and decide which pack to expand while performing code synthesis by expanding that pack (rather than performing the expansion itself).

Property packs

Property packs are a foundational construct that could support features well beyond memberwise initialization. For example, property packs could be considered as an alternative to user-defined attributes in specifying which properties participate in Equatable, Hashable and Codable conformances (of course the pros and cons of each approach would need to be weighed carefully). This future direction is covered at the end of the document.

Explicit memberwise initialization provide a relatively straightforward and common use case in wheich to familiarize Swift programmers with the notions of packs and expansions. This should help to make other powerful language features built on packs and expansions (such as variadic generics) feel more accessible than they otherwise might.

For the purposes of this proposal, property packs are purely compile-time constructs with no runtime representation.

Implicit property packs

As any other identifier, a lexical context may define implicit property packs. This is analogous to newValue in setters, oldValue in property observers and the $ identifiers in closures. The implicit access level property pack identifiers are defined contextually for struct and class initializers.

The implicit individual property packs specified by this proposal may be defined this way as well. An alternative design would specify that an individual property pack is always defined for every property. Under this alternative we would need the ability to distinguish stored properties from computed properties. For example, only stored property packs should be supported by initializers, but if the future direction of allowing property packs in method signatures is pursued it may make sense to allow computed properties with setters as well, but we cannot support packs including let properties (which are supported in initializers). We should leave this unspecified until we have the opportunity to consider additional use cases for property packs.

Access control

Access control rules for the implicit property packs are applied as one would expect. An initializer declared in a different file may not use the implicit private or fileprivate property packs. An initializer declared in a different module may not use those and also may not use the implicit internal property pack. Availability of individual property packs depend on the visibility of the property itself. In other words, if you cannot write the initializer manually you cannot write it using the implicit property packs.

Grammar changes

TODO

Source compatibility

This proposal is purely additive.

With one exception an initializer that uses pack expansions is identical to writing the equivalent initializer manually. The one exception is that it will not be possible to manually write an initializer that suppresses initial value assignment without using pack expansion. The ability to manually implement these initializers with identical behavior to the form that uses pack expansions is discussed as a future direction below.

Effect on ABI stability

As a syntactic sugar proposal there is no impact on ABI stability.

Effect on API resilience

As a syntactic sugar proposal there is no impact on API resilience.

Alternatives considered

Prefix implicit property packs with #

As with other compile-time constructs, we could spell the implicit property packs #private, #internal, etc (#private... and #internal... respectively when expanded). This may well be the best design. It should be considered carefully.

Introduce a memberwise keyword to mark memberwise initializers

We could restrict the usage of property packs to initializers marked as memberwise. This adds boilerplate and complexity where none is necessary. Introducing the notion of property packs that can be used by any initializer is a simpler, more general model.

Different semantics for the implicit property packs

We could define the implicit property packs to include only the stored properties at the exact access level as the identifier used to name the pack. Pack composition would be used when properties from multiple access levels need to be expanded: (public | internal).... This approach was not chose for several reasons:

  • this approach will be more verbose in common scenarios
  • listing out higher access levels is less important than highlighting the lowest access level that is being expanded
  • the implicit property packs already exclude computed properties and all superclass properties
  • the implicit property packs are only available in initializer signatures so their semantics should be tailored to anticipated needs in this context
  • this design aligns the name of the property pack with the visibility of the current memberwise initializer (which can now be written explicitly using the pack)

There are very good reasons to choose this set of semantics.

Adopt an entirely different approach

The core team analyzed many approaches to memberwise initialization in the rationale provided when SE-0018 was deferred. All of the approaches and analysis described in that document were considered and informed the present design.

Following the review of SE-0018 I also provided an analysis that explored some new directions that were discussed during and inspired by the review of SE-0018. These approaches were also considered and informed the present design.

Future directions

Explicit property packs

We could allow programmers to explicitly create property packs by using an @pack(packName1, packName2) attribute on properties. This could be useful in cases where several initializers need to expose the same group of memberwise parameters but they don't fall together into one of the implicit property packs.

Initial value assingment suppression

Property pack expansion replaces the initial value assignment for any @initializable let or var property included in the expansion. We could extend the ability to replace initial value assignment to any initializer (including memberwise initializers that want to explicitly initialize one of these properties instead of using a property pack). This could be supported using @initializes(property1, property2).

In addition to being useful, this would reduce the magic involved in memberwise initialization by providing syntax that allows us to explicitly write out the expansion of a memberwise initializer if desired.

Support property packs in method signatures

We could allow property packs of var properties with setters (including computed properties with setters) to be expanded in method signatures. This is an interesting direction to consider. In some sense it would provide symmetry with initializers but as noted in the detailed design, the rules about what expansions are supported would need to be somewhat different.

Support property packs in other contexts

As with initialization, property pack expansion could reduce the steep cliff from synthesis to a full manual implementation in many areas. For example, it may be interesting to explore a direction that supports writing a wide range of memberwise expression or statement chains using property pack expansions.

public static func == (_ lhs: Self, _ rhs: Self) -> Bool {
   return (lhs.#packIdentifier == rhs.#packIdentifier &&)...
}
public func hash(into hasher: inout Hasher) {
   hasher.combine(#packIdentifer)...
}

Note: the syntax in the above example is a strawman only used to demonstrate the idea, it is not intended to represent a well thought out design.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment