[swift-evolution] [Proposal Draft] Flexible memberwise initialization

Matthew Johnson matthew at anandabits.com
Mon Dec 21 13:32:58 CST 2015


I have completed a draft of the proposal I have been working on for flexible memberwise initialization.  I am really looking forward to your input and will be refining the proposal based on our discussion.

I am including a current snapshot of the proposal in this message.  I will keep the proposal up to date on Github at this link:

https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md>

Flexible Memberwise Initialization

Proposal: SE-NNNN <https://github.com/apple/swift-evolution/blob/master/proposals/NNNN-flexible-memberwise-initializers.md>
Author(s): Matthew Johnson <https://github.com/anandabits>
Status: Review
Review manager: TBD
 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#introduction>Introduction

The Swift compiler is currently able to generate a memberwise initializer for us in some circumstances however there are currently many limitations to this. This proposal build on the idea of compiler generated memberwise initialization making it available to any initializer that opts in.

 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#motivation>Motivation

When designing initializers for a type we are currently faced with the unfortunate fact that the more flexibility we wish to offer users the more boilerplate we are required to write and maintain. We usually end up with more boilerplate and less flexibility than desired. There have been various strategies employed to mitigate this problem. 

Sometimes properties that should be immutable are made mutable and a potentially unsafe ad-hoc two-phase initialization pattern is employed where an instance is initialized and then configured immediately afterwards. When properties that need to be mutable have a sensible default value they are simply default-initialized and the same post-initialization configuration strategy is employed when the default value is not correct for the intended use. This results in an instance which may pass through several incorrect states before it is correctly initialized for its intended use.

Flexible and concise initialization for both type authors and consumers will encourages using immutability where possible and removes the need for boilerplate from the concerns one must consider when designing the intializers for a type.

Quoting Chris Lattner <https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20151130/000518.html>:

The default memberwise initializer behavior of Swift has at least these deficiencies (IMO):
1) Defining a custom init in a struct disables the memberwise initializer, and there is no easy way to get it back.
2) Access control + the memberwise init often requires you to implement it yourself.
3) We don’t get memberwise inits for classes.
4) var properties with default initializers should have their parameter to the synthesized initializer defaulted.
5) lazy properties with memberwise initializers have problems (the memberwise init eagerly touches it).
Add to the list “all or nothing”. The compiler generates the entire initializer and does not help to eliminate boilerplate for any other initializers where it may be desirable to use memberwise intialization for a subset of members and initialize others manually.

It is common to have a type with a number of public members that are intended to be configured by clients, but also with some private state comprising implementation details of the type. This is especially prevalent in UI code which may expose many properties for configuring visual appearance, etc. Flexibile memberwise initialization can provide great benefit in these use cases, but it immediately becomes useless if it is "all or nothing". 

We need a flexible solution that can synthesize memberwise initialization for some members while allowing the type auther full control over initialization of implementation details.

 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#proposed-solution>Proposed solution

I propose adding a memberwise declaration modifier for initializers which allows them to opt-in to synthesis of memberwise initialization and a @nomemberwise property attribute allowing them to opt-out of such synthesis. 

This section of the document contains several examples of the solution in action. Specific details on how synthesis is performed are contained in the detailed design.

 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#replacing-the-current-memberwise-initializer>Replacing the current memberwise initializer

struct S {
    let s: String
    let i: Int

    // user declares:
    memberwise init() {}
    // compiler synthesizes:
    init(s: String, i: Int) {
        self.s = s
        self.i = i
    }
}
 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#properties-with-initial-values>Properties with initial values

struct S {
    let s: String = "hello"
    let i: Int = 42

    // user declares:
    memberwise init() {}
    // compiler synthesizes:
    init(s: String = "hello", i: Int = 42) {
        self.s = s
        self.i = i
    }
}
 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#partial-memberwise-initialization>Partial memberwise initialization

struct S {
    let s: String
    let i: Int

    // user declares:
    memberwise init() {
        i = getTheValueForI()
    }
    // compiler synthesizes (suppressing memberwise initialization for properties assigned in the initializer body):
    init(s: String) {
        self.s = s
        // body of the user's initializer remains
        i = getTheValueForI()
    }
}
 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#access-control>access control

struct S {
    let s: String
    private let i: Int

    // user declares:
    memberwise init() {
        // compiler error, i memberwise initialization cannot be synthesized 
        // for i because it is less visible than the initializer itself
    }
}
 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#lazy-properties-and-incompatible-behaviors>lazy properties and incompatible behaviors

struct S {
    let s: String
    lazy var i: Int = InitialValueForI()

    // user declares:
    memberwise init() {
    }
    // compiler synthesizes:
    init(s: String) {
        self.s = s
        // compiler does not synthesize initialization for i 
        // because it contains a behavior that is incompatible with 
        // memberwise initialization
    }
}
 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#nomemberwise-properties>@nomemberwise properties

struct S {
    let s: String
    @nomemberwise let i: Int

    // user declares:
    memberwise init(configuration: SomeTypeWithAnIntMember) {
        i = configuration.intMember
    }
    // compiler synthesizes:
    init(configuration: SomeTypeWithAnIntMember, s: String) {
        self.s = s
        i = configuration.intMember
    }
}
 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#uninitialized-nomemberwise-properties>uninitialized @nomemberwise properties

struct S {
    let s: String
    @nomemberwise let i: Int

    // user declares:
    memberwise init() {
        // compiler error, i is not initialized
    }
}
 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#delegating-and-convenience-initializers>delegating and convenience initializers

struct S {
    let s: String = "hello"
    let i: Int = 42

    // user declares:
    memberwise init() {}
    // compiler synthesizes:
    init(s: String = "hello", i: Int = 42) {
        self.s = s
        self.i = i
    }

    // user declares:
    memberwise init(describable: CustomStringConvertible) {
        self.init(s: describable.description)
    }
    // compiler synthesizes (adding forwarded memberwise parameters):
    init(describable: CustomStringConvertible, i: Int = 42) {
        self.init(s: describable.description, i: i)
    }
}
 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#subclass-initializers>subclass initializers

class Base {
    let baseProperty: String

    // user declares:
    memberwise init() {}
    // compiler synthesizes:
    init(baseProperty: String) {
        self.baseProperty = baseProperty
    }
}

class Derived: Base {
    let derivedProperty: Int

    // user declares:
    memberwise init() {}
    // compiler synthesizes (adding forwarded memberwise parameters):
    init(baseProperty: String, derivedProperty: Int) {
        self.derivedProperry = derivedProperty
        super.init(baseProperty: baseProperty)
    }
}
 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#detailed-design>Detailed design

 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#syntax-changes>Syntax changes

This proposal introduces two new syntactic elements: the memberwise initializer declaration modifier and the @nomemberwise property attribute.

Initializers will be able to opt-in to synthesized memberwise initialization with the memberwise declaration modifier. This modifier will cause the compiler to follow the procedure outlined later in the design to synthesize memberwise parameters as well as memberwise initialization code at the beginning of the initializer body.

Properties will be able to opt-out of memberwise initialization with the @nomemberwise attribute. When they do so they will not be eligible for memberwise initialization synthesis. Because of this they must be initialized directly with an initial value or initialized directly by every initializer for the type.

 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#overview>Overview

Throughout this design the term memberwise initialization parameter is used to refer to initializer parameters synthesized by the compiler as part of memberwise initialization synthesis.

 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#algorithm>Algorithm

The steps described in this section will be followed by the compiler when it performs memberwise initialization synthesis. These steps supercede the synthesis of initialization for properties with initial values that exists today.

When the compiler performs memberwise initialization synthesis it will determine the set of properties that are eligible for synthesis that are not directly initialized in the body of the initializer. It will then synthesize parameters for them as well the initialization of them at the beginning of the initializer body.

 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#terminology>Terminology

direct memberwise initialization parameters are parameters which are synthesized by the compiler and initialized directly in the body of the initializer.

forwarded memberwise initialization parameters are parameters which are synthesized by the compiler and provided to another initializer that is called in the body of the initializer.

synthesized memberwise initialization parameters or simply memberwise initialization parameters is the full set of parameters synthesized by the compiler which includes both direct and forwarded memberwise initialization parameters.

 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#designated-initializers-and-non-delegating-struct-initializers>Designated initializers and non-delegating struct initializers

Determine the set of properties elibile for memberwise initialization synthesis. This set is known as the set of direct memberwise initialization parameters. In order to be eligible for memberwise initialization synthesis a property must be at least as visible as the initializer itself, must not have the @nomemberwise attribute, and must not have a behavior that does not allow memberwise initialization. Currently lazy is an example of such a behavior that should prohibit memberwise initialization. If both this proposal and the Property Behaviors proposal are accepted we will need a way for behaviors to specify whether they are compatible with memberwise initialization or not.

If any of the properties in that set produced in step one are directly initialized in the body of the initializer or have a name identical to an external parameter name for the intitializer remove them from the set. If the initializer contains a parameter with an external label matching the name of a property that is eligible for memberwise intialization it must initialize that property directly.

When performing memberwise initialization for a subclass, inspect the call it makes to its superclass initialzier. Determine the set of synthesized memberwise initialization parameters that exist for the superclass initializer that is called. These parameters may participate in memberwise initialization parameter forwarding. The set is known as the set of forwarded memberwise initialization parameters.

If the subclass initializer provides arguments for any of the parameters identified in step three remove them from the set. Because a value is provided for them directly synthesized forwarding is not necessary.

If the superclass property corresponding to any of the remaining forwarded memberwise initialization parameters has a lower visibility than the initializer itself report a compilation error. These parameters must be supplied directly by the subclass initializer.

Divide each of the sets gathered in step one and step three into two subsets, one of properties that contain initial values and the other containing properties that do not contain initial values.

Synthesize memberwise initialization parameters at the end of the initializer parameter list, but immediately prior to a trailing function parameter if such a parameter exists. The synthesized parameters should have external labels matching the property name. Place the synthesized parameters in the following order:

forwarded memberwise initialization parameters that do not have an initial value in the same order they appear in the superclass initializer.
direct memberwise initialization parameters that do not have an initial value in the order in which their corresponding properties are declared.
forwarded memberwise initialization parameters that do have an initial value in the same order they appear in the superclass intitializer. Also synthesize a default value matching the initial value for these parameters.
direct memberwise initialization parameters that do have an initial value in the order in which their corresponding properties are declared.
Synthesize initialization of all direct memberwise initialization parameters at the beginning of the initializer body.

Synthesize the initialization of any properties which are ineligible for memberwise initialization, are not initialized elsewhere in the initializer body, and which do have an initial value provided in their declaration. This step is identical to the synthesis of initialization for properties that declare initial values that happens today, but applies to a more restricted set of properties: those which are not initialized directly and are not eligible for memberwise initialization synthesis (when the initializer contains the memberwise declaration modifier). 

ASIDE: it would be desirable to suppress the synthesis of properties that declare an initial value if that property is initialized directly in the body of the initializer whether or not the initializer opts-in to memberwise initialization. This does not currently happen today, making it impossible to override an initial value for immutable properties with a different value in the body of an initializer.

Synthesize arguments to the superclass initializer for forwarded memberwise initialization parameters. The call to the superclass initializer in the memberwise initializer body must be updated to forward any forwarded memberwise initialization parameters that were synthesized by the compiler.

 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#convenience-and-delegating-initializers>Convenience and delegating initializers

Convenience initializers for classes and delegating initializers use the same algorithm for forwarding memberwise initialization parameters as described in the previous steps. They do not include any direct memberwise initialization parameters and do not synthesize initialization of any stored properties in the body of the initializer.

 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#objective-c-class-import>Objective-C Class Import

Objective-C frameworks are extremely important to (most) Swift developers. In order to provide the call-site advantages of flexible memberwise initialization to Swift code using Cocoa frameworks this proposal recommends introducing a MEMBERWISE attribute that can be applied to Objective-C properties and initializers.

Mutable Objective-C properties can be marked with the MEMBERWISE attribute. Readonly Objective-C properties cannot be marked with the MEMBERWISE attribute. The MEMBERWISE attribute should only be used for properties that are initialized with a default value (not a value provided directly by the caller or computed in some way) in all of the class's initializers.

Objective-C initializers may also be marked with the MEMBERWISE attribute. When Swift imports an Objectiv-C initializer marked with this attribute it allows callers to provide memberwise values for the properties declared in the class that are marked with the MEMBERWISE attribute. At call sites for these initializers the compiler performs a transformation that results in the memberwise properties being set with the provided value immediately after initialization of the instance completes.

It may also be desirable to allow specific initializers to hide the memberwise parameter for specific properties if necessary. NO_MEMBERWISE(prop1, prop2)

It is important to observe that the mechanism for performing memberwise initialization of Objective-C classes (post-initialization setter calls) is implemented in a different way than native Swift memberwise initialization. As long as developers are careful in how they annotate Objective-C types this implementation difference should not result in any observable differences to callers. 

The difference in implementation is necessary if we wish to use call-site memberwise initialization syntax in Swift when initializing instances of Cocoa classes. There have been several threads with ideas for better syntax for initializing members of Cocoa class instances. I believe memberwise initialization is the best way to do this as it allows full configuration of the instance in the initializer call. 

Obviously supporting memberwise initialization with Cocoa classes would require Apple to add the MEMBERWISE attribute where appropriate. If this proposal is accepted with the Objective-C class import provision intact my hope is that this will happen as it has in other cases where annotations are necessary to improve Swift interoperability. If Apple does not intend to do so it may be desirable to remove the Objective-C interop portion of this proposal.

 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#impact-on-existing-code>Impact on existing code

The changes described in this proposal are strictly additive and will have no impact on existing code.

One possible breaking change which may be desirable to include alongside this proposed solution is to elimintate the existing memberwise initializer for structs and require developers to specifically opt-in to its synthesis by writing memberwise init() {}. A mechanical transformation is possible to generate this declaration automatically if the existing memberwise initializer is removed.

 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#alternatives-considered>Alternatives considered

 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#require-stored-properties-to-opt-in-to-memberwise-initialization>Require stored properties to opt-in to memberwise initialization

This is a reasonable option and and I expect a healthy debate about which default is better. The decision to require opt-out was made for several reasons:

The memberwise initializer for structs does not currently require an annotation for properties to opt-in. Requiring an annotation for a mechanism designed to supercede that mechanism may be viewed as boilerplate.
Stored properties with public visibility are often intialized directly with a value provided by the caller.
Stored properties with less visibility than a memberwise initializer are not eligible for memberwise initialization. No annotation is required to indicate that.
I do think a strong argument can be made that it may be safer and more clear to require an @memberwise attribute on stored properties in order to opt-in to memberwise initialization. I am very interested in community input on this.

 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#allow-all-initializers-to-participate-in-memberwise-initialization>Allow all initializers to participate in memberwise initialization

This option was not seriously considered. It would impact existing code and it would provide no indication in the declaration of the initializer that the compiler will synthesize additional parameters and perform additional initialization of stored properties in the body of the initializer.

 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#require-initializers-to-opt-out-of-memberwise-initialization>Require initializers to opt-out of memberwise initialization

This option was also not seriously considered. It has the same problems as allowing all initializers to participate in memberwise initialization.

 <https://github.com/anandabits/swift-evolution/blob/flexible-memberwise-initialization/proposals/NNNN-flexible-memberwise-initialization.md#require-initializers-to-explicitly-specify-memberwise-initialization-parameters>Require initializers to explicitly specify memberwise initialization parameters

The thread "helpers for initializing properties of the same name as parameters <https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20151130/000428.html>" discussed an idea for synthesizing property initialization in the body of the initializer while requiring the parameters to be declard explicitly. 

struct Foo {
    let bar: String
    let bas: Int
    let baz: Double
    init(self.bar: String, self.bas: Int, bax: Int) {
          // self.bar = bar synthesized by the compiler
          // self.bas = bas synthesized by the compiler
        self.baz = Double(bax)
    }
}
The downside of this approach is that the boilerplate parameter declarations grow at the rate MxN (properties x initializers). It also does not address forwarding of memberwise initialization parameters which makes it useless for convenience and delegating initializers.

Proponents of this approach believe it provides additional clarity and control over the current proposal. 

Under the current proposal full control is still available. It requires initializers to opt-in to memberwise initialization. When full control is necessary an initializer will simply not opt-in to memberwise initialization synthesis. The boilerplate saved in the examples on the list is relatively minimal and is tolerable in situations where full control of initialization is required.

I believe the memberwise declaration modifier on the initializer makes it clear that the compiler will synthesize additional parameters. Furthermore, IDEs and generated documentation will contain the full, synthesized signature of the initializer.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20151221/13786e31/attachment.html>


More information about the swift-evolution mailing list