[swift-evolution] [Review] SE-0018 Flexible Memberwise Initialization

Matthew Johnson matthew at anandabits.com
Thu Jan 14 08:35:25 CST 2016


I want to thank the core team for putting so much thought and consideration into their feedback.  This was clearly given extensive discussion.  I also want to appologize if it proved to be a distraction from more important goals.

I agree with the recommendation the team has made.  I began to be concerned about the "complexity" of the “automatic" model prior to the start of the review and those concerns proved to be shared by many.  Using an "opt-in" approach will provide much-needed clarity.  I am especially happy to see that the core team has decided that supporting default parameter values for `let` properties is an important aspect of an eventual solution.

There is one thing that isn't clear to me in the feedback.  Does the core team believe the implicit memberwise initializer should: 

1) Remain in its current form
2) Remain in a slightly enhanced form (i.e. receive default parameter values)
3) Remain in an enhanced form and also be extended to classes
4) Be removed in favor of making all initilaizers explicitly declared

As requested, I will defer work on a modified formal proposal until the time is right.  Chris, please let me know when you're ready to take this topic up again.  I will draft a proposal based on the "opt-in" approach (with no future enhancements!).

In the meantime, anyone who is interested in seeing an outline of possible options for an "opt-in" approach that builds on more general initialization features should keep an eye out for the second (first complete) draft of my Partial Initializers proposal.  At John McCall's request I am including an a section that describes how partial initializers could be related to memberwise intialization.  I hope to have the draft ready later today.

Thank you again to everyone who participated in the discussion and review of my proposal.  It has been a very productive conversation.   My thinking about the topic has been refined and clarified significantly by this process.

- Matthew



> On Jan 13, 2016, at 1:24 AM, Chris Lattner via swift-evolution <swift-evolution at swift.org> wrote:
> 
> On Jan 6, 2016, at 2:47 PM, Chris Lattner via swift-evolution <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
>> Hello Swift community,
>> 
>> The review of "Flexible Memberwise Initialization" begins now and runs through January 10th. The proposal is available here:
>> 
>> 	https://github.com/apple/swift-evolution/blob/master/proposals/0018-flexible-memberwise-initialization.md <https://github.com/apple/swift-evolution/blob/master/proposals/0018-flexible-memberwise-initialization.md>
> 
> Here are some long and poorly organized notes from the core team meeting to discuss this feature.  Many, many, thanks to Alex Martini for capturing many of these details, but the mess I’ve made of this is not his fault. :-)
> 
> 
> --- Metapoints --- 
> 
> First, some meta points that came up from several members of the core team:
> 
> - Less is more.  Many folks (core team and other community members both) commented that the mere presence of the expansive “future directions” section made the proposal off-putting.
> 
> - “Pure sugar” proposals need to have strong justification for their existence since they do not add new capabilities, but they do add new complexity to the language.
> 
> - This feature included justification based on the fact that the existing memberwise feature has problems, but some core team folks thought that this wasn’t good enough justification.  They commented that perhaps we should just remove the existing memberwise init for structs and call it a day.  (others strongly disagreed :-)
> 
> - The team wanted to evaluate “all” of the conceivable different approaches to make sure that this approach was the right one in general.  I captured notes about some of that discussion below.  On the balance, they agree that a variant of this proposal is the right way to go.
> 
> - We didn’t have time to bikeshed the fine details of the proposal (e.g. the “memberwise” keyword, the … sigil, etc), just discussing the general semantics and shape of what an acceptable solution would look like.
> 
> 
> MOST SIGNIFICANTLY: there was strong pushback about considering this proposal *at all* right now: this is a pure sugar proposal with a broad design space.  It doesn’t have long term ABI implications at all, and we have other serious semantic problems in the language to clean up and address.  We can tackle this sort of sugar feature at any time, including in Swift 4.  The core team would like to defer talking about this for a couple of months until the other things (property behaviors, resilience, and other big changes) are behind us.  We need to focus on the important things, because getting syntactic niceties into Swift 3 but missing on ABI stability would be really unfortunate.  We have seriously constrained design and engineering bandwidth.  This feedback applies to other pure-sugar proposals as well.
> 
> 
> --- Rationale --- 
> 
> I explained three bits of rationale for why we should tackle this:
> 
> 1) Our existing feature is half-baked and unsatisfying.  This causes me personal angst but, as pointed out above, the simplest solution is to just remove what we have until we can do it right.
> 
> 2) Memberwise init sugar strongly benefits “POD” types and other “bags of property” types (e.g. “Vec4"), and many of the C struct types that Cocoa has (CGRect etc).  In these cases, clients often want to initialize all of the fields explicitly and a memberwise init proposal eliminates this boilerplate.  This case is what our existing feature attempts to service.
> 
> 3) Memberwise init sugar can replace some really common “builder” patterns.  These cases often have a bunch of configurable state in a class, where almost all of it has default values, but where clients want to be able to only specify deltas from the default.  In the cases where you have a type with a ton of data members, a memberwise init is very very appealing because it eliminates a ton of boilerplate to set up a builder manually, and it also allows the captured state to be immutable (if we allow memberwise init of lets, which Matthew is a huge proponent of).  This case can often be a class derived from something else (e.g. a Kit class), explaining the desire to specify super.init, add custom parameters, etc.
> 
> 
> #3 is my distillation/abstraction of many of the arguments that Matthew has made on the list and in the proposal (e.g. his very helpful FontPicker example), but I don’t claim that this captures all of his or any one else’s motivation.  This is simply the rationale that the team discussed.
> 
> 
> --- Common feedback and points ---
> 
> Getting to points common to any “memberwise initializer” proposal, the goals of the core team are:
> 
> - Provide a predictable model.  It was concerning to many people that you could accidentally reorder properties and break a memberwise init without knowing/thinking about it.  This directly contravenes the resilience model.  API diff’ing tools and “synthesized header” views in Xcode ameliorate this, but don’t totally solve it.
> 
> - After discussion, the desire to support memberwise init of let properties was found to be a strong goal.  The entire win of providing a builder-like pattern in an init method (vs doing initialize to default and reassign over the value later, C# style) is the immutability benefits.
> 
> - We would like for memberwise init to be orthogonal to property behaviors, access control, attributes, and all the other stuff you’d want to do on your properties.  Also, ideally, you shouldn’t be limited to what kinds of properties can participate.
> 
> - The issues around whether properties with a default value (both let and var) can be replaced by a memberwise init is a really important semantic decision we need to nail down.  Many of the proposals include the problem/fact that something like:
> 
> class C {
>   var x : Int = foo()
> 
>   memberwise init(…) {}
> }
> 
> would not run the foo() side effect when initialized with "C(42)”.  Similarly, in a case like:
> 
> class C {
>   let x : Int = 42
>   memberwise init(…) {}
> }
> 
> it is still very concerning to people that the apparent axiom that “x is 42” could be violated by a memberwise init.  Allowing lets have other implementation issues, such as suddenly making “x” require storage, where it could otherwise be optimized out.
> 
> 
> 
> OTOH, my previous objection about being able to write a memberwise init manually is bogus, since you could write the above out like this:
> 
> class C {
>   let x : Int     // initial value removed.
>   init(x: Int) { self.x = x }   // manually written memberwise init.
>   init() { x = 42 }  // move the inline initial value here.
> }
> 
> 
> 
> --- Approaches discussed ---
> 
> Here are the rough approaches we discussed, along with pros and cons.  For consistency, most of them are modeling the analog of sugarizing an example case like this:
> 
> class C : Derived {   // has base class
>     var x: Int               // no default value.
>     var y = 4.0            // default value
>     var z = “foo"          // default value
> 
>      // has an explicit “q” parameter as well as all the members as parameters.
>      init(q: Int, x: Int, y: Double = 4.0, z: String = "foo”) {
>         self.x = x 
>         self.y = y
>         self.z = z
>         // your custom code can go here
>         super.init(q)
>     }
> }
> 
> I ordered this so that the most promising ones are at the end, to keep you all in suspense :-)
> 
> 
> Approach: C# model
> 
> As pointed out on the list, we could eliminate the notion of a synthesized memberwise init, and move the complexity to the caller side.  C# uses syntax along the lines of:
> 
> 	new Foo(){x=42, y=17.0}
> 
> pro) No memberwise initializer concept
> con) Requires all fields to be default initialized, which breaks non-nullable pointers etc.
> con) Requires mutability, since you’re initializing, and then overwriting a field.
> con) This specific syntax would be problematic with trailing closures etc.
> 
> Core team was strongly opposed to this approach.
> 
> 
> Approach: Scala (among others) memberwise init as part of class decl
> 
> This would give us something like this:
> 
> class C(var x: Int, y: Float = 4.0, var z : String = “foo") : Derived {
>     var extra = 4.0   // extra, non-memberwise fields supported.
>     init(q: Int, ...) {  // all initializers get memberwise stuff added, allows bodies.
>        extra = 17
>         // your custom code can go here
>         super.init(q)
>      }
> }
> 
> pro) Very predictable model, makes it very clear what is subject to memberwise init, and that the order matters
> pro) This supports indicating that some fields don’t participate in memberwise init.
> pro?) Syntax looks vaguely like the parameter list of the init
> con) The syntax isn’t actually parameter list syntax, it is property syntax.  You’d want to accept public/private, let/var, behaviors, attributes, and lots of other stuff in there.  This would introduce confusion about parameter lists.
> con) This is sticking more junk into the class decl, which is already populated with class attributes/modifiers, subclasses/protocols, etc.
> con) This means we’ve have two very different ways to declare stored properties in a type.
> 
> Core team was strongly opposed to this approach.
> 
> 
> Approach: Extract the members from the parameters of an initializer decl
> 
> This would give us something like this:
> 
> class C : Derived {
>     // members come from the memberwise init.
>      memberwise init(x: Int, y: Double = 4.0, z: String = "foo”) {
>         // your custom code can go here
>         super.init(q)
>     }
> }
> 
> pro) Syntactically very terse.
> con) This only supports one memberwise init.
> con) Two ways to declare stored properties
> con) Conflates the property grammar (behaviors etc) with parameter grammar, many of the same problems as approach right above does.
> 
> Core team was strongly opposed to this approach.
> 
> 
> Approach: barely extend our existing model:
> 
> We could put default values onto our existing memberwise init, and allow you to write "memberwise init(...)” to explicitly request it to be synthesized, but not allow a body.  We could bring the existing level of feature to classes.
> 
> pro) simple, solves some problems with the existing model.
> pro) brings memberwise init to root classes and classes that derive from something with a init() to chain to (e.g. NSObject).
> con) doesn’t solve enough of the problem to be worth it
> con) feels like we are perpetuating a hack.
> con) we’d have to extend this further at some point, this is just kicking the can down the road.
> 
> Core team was strongly opposed to this approach.  Better to either rip out what we have or do a bigger approach.
> 
> 
> Approach: Magic “members” tuple (or other generics/variadics features) 
> 
> class C : Derived {
>     var x: Int
>     var y = 4.0
>     var z = “foo"
> 
>     init(p : … Members) {
>        self.members = p
>     }
> 
>     // compiler synthesizes.
>     typealias Members = (Int, Float, String)
>     var members : Members { get {..} set {..}}
> }
> 
> pro) seems like a more generally useful feature that could pay for itself in other ways
> con) doesn’t work with default values, so it doesn’t solve the builder problem at all (and no, we are not adding default values back to tuple types :-)
> 
> Core team was strongly opposed to this as an approach for memberwise init, but can always consider doing this for other things if it is motivated by them.
> 
> 
> Approach: Introduce sugar for “self.x = x"
> 
> David Owens ][ mentioned that perhaps we can go with something simpler, like this (concrete syntax is just a strawman):
> 
> class C : Derived {
>     var x: Int
>     var y = 4.0
>     var z = “foo"
> 
>      init(q: Int, self.x, self.y, self.z) { 
>         // your custom code can go here
>         super.init(q)
>     }
> }
> 
> … where self.x in the parameter list is sugar that says it takes whatever the x property is (incl type and initial value if present), and does "self.x = x” in the body.
> 
> pro) Does not require introducing the notion of a memberwise init.  Any init could use this.
> pro) Makes it very clear what members are being initialized and what the API for the init is.
> pro) Makes it easy to pick and choose which members you want to make available to clients.
> con) This isn’t sweet enough sugar to be worthwhile, particularly for the builder pattern case, which would require lots of boilerplate
> con) the self.x syntax is weird and surely can be improved, but anything we came up with would be magic and weird.  Making this keyword driven (e.g. "memberwise” at least gives people something to google and lets them know they’re looking at something magic).
> con) doesn’t address the problems of replacing the var/let initial values (zapping a side effect in either of them, and breaking ‘let’ axioms).
> 
> Core team was weakly opposed to this approach, but agree that something like this could be composed on top of another proposal if that made sense.
> 
> 
> Approach: Introduce sugar for “self.x = x”, and add “memberwise and …"
> 
> Extending David Owens ][’s model a bit, we could keep the behavior he indicates and add memberwise/… to handle cases with default values:
> 
> class C : Derived {
>     var x: Int
>     var y = 4.0
>     var z = “foo"
> 
>      memberwise init(q: Int, self.x, ...) { 
>         // your custom code can go here
>         super.init(q)
>     }
> }
> 
> … where self.x in the parameter list is sugar that says it takes whatever the x property is (incl type and initial value if present), and does "self.x = x” in the body.  The presence of memberwise/… would forward all the properties with default values.
> 
> pro) Since default values can be provided by a callee in any order, this approach can be made stable against reordering of properties.
> pro) Compared to the base proposal above, this would be great for the builder pattern case.
> con) The POD case still requires you to duplicate all your properties, so this doesn’t really address that use-case.
> con) the self.x syntax is weird and surely can be improved, but anything we came up with would be magic and weird.
> con) doesn’t address the problems of replacing the var/let initial values (zapping a side effect in either of them, and breaking ‘let’ axioms).
> 
> Core team was weakly opposed to this approach.
> 
> 
> Approach: Matthew’s proposal SE-0018 (the actual formal proposal :)
> 
> class C : Derived {
>     var x: Int
>     var y = 4.0
>     var z = “foo"
> 
>      memberwise init(q: Int, ...) {
>         // your custom code can go here
>         super.init(q)
>     }
> }
> 
> pro) Solves the POD case as well as the builder case.
> pro) Keeps properties the way they are, composes with behaviors etc.
> pro) Supports adding custom parameters to the initializer, supports custom chaining to super.init, etc.
> pro) Supports multiple different memberwise inits in a class, e.g. that want to chain to different super.init’s.
> pro) Like the clarity of having a keyword on the init, and a sigil in the parameter list saying that something is going on.
> unclear) I’m not delving into it here, but the access control aspects were very controversial.  We didn’t discuss it fully.
> con) compared to the Scala approach, doesn’t handle the ability to opt a member out of memberwise init (we’d need some thing like @nomemberwise) 
> con) Doesn’t address the problems of replacing the var/let initial values (zapping a side effect in either of them, and breaking ‘let’ axioms).
> con) Lots of concerns about unclarity that the order of the properties matter.  In a large type, the memberwise init could be “at the end” of the class, after a bunch of other initializers and methods, and the properties could be “up at the top”.  It could be very unclear to people that memberwise init is even happening, and changing a property could have surprising effects.
> con) Not clear from the memberwise init declaration what the extra parameters are, you have to look at a synthesized header or something.
> 
> Core team thought this was very close, but the problems were significant enough to think it won’t fly, and suggest pursuing the variant below.
> 
> 
> Approach: "Opt-in" version of Matthew’s proposal
> 
> Matthew has mentioned on list that he likes opt-in models, and after discussion the core team agreed that it is more promising than the base proposal.
> 
> In addition to the memberwise declmodifier, *also* introduce a declmodifier on the properties in question.  Here I’ll use “mwi” as a strawman for “memberwise initializerable”, but it is obviously not the right thing, and I’m sure it will be replaced with something else brilliant :-)
> 
> class C : Derived {
>     mwi var x: Int
>     mwi var y = 4.0
>     mwi var z = “foo"
>     var extra = 4.0   // Doesn’t participate in memberwise inits.
> 
>      memberwise init(q: Int, ...) {
>         // your custom code can go here
>         super.init(q)
>     }
> }
> 
> pro) Solves the POD case as well as the builder case.
> pro) Keeps properties the way they are, composes with behaviors etc.
> pro) Supports adding custom parameters to the initializer, supports custom chaining to super.init, etc.
> pro) Supports multiple different memberwise inits in a class, e.g. that want to chain to different super.init’s.
> pro) Like the clarity of having a keyword on the init, and a sigil in the parameter list saying that something is going on.
> pro) as with the Scala approach it handles the ability to have members be both memberwise and non-memberwise participating.
> pro) Works with let and var initializer semantics.
> pro) Looking at a property, you know immediately that it participates in memberwise init, and that you should go update the memberwise inits if you move it or change it.  We could warn/error if you have a “mwi” property and no memberwise init, or a memberwise init with no mwi property.
> pro) because this is an opt-in model, we can eliminate the controversial access control aspect of SE-0018.
> con) Not clear from the memberwise init declaration what the extra parameters are, you have to look at a synthesized header or something.
> con) more verbose than Matthew’s proposal.  OTOH, the verbosity isn’t “boilerplate" since we’re getting something out of it. Simple POD cases can be written as (e.g.) “mwi var x, y, z, w : Double” which isn’t bad.
> 
> Adding the modifier on the affected property decls helps improve the intentionality and eliminate some of the "spooky action at a distance" problem, but the major breakthrough (IMO) is that it allows us to include lets into the model, and gives us a simple answer for why side effects are squashed on vars.  Consider this example:
> 
> class C { 
>   mwi let a = 42
>   mwi var b : Int = sideeffect()
> 
>   memberwise init(…) {}
> }
> 
> A call site would look like this, giving A a value of 75 and would not run the side effect:
>  let tmp = C(a: 75, b: 12)
> 
> We’re ok with the side effect being squashed, and the let axiom being broken given that there is a decl modifier on the property decl.  This is because you can tell locally by looking at the property that a memberwise initializer provides a different value.  From an implementation perspective, this is also pretty great, because this leads to a very straight-forward codegen model that cleanly meshes with what we already have.
> 
> 
> --- A path forward --- 
> 
> So that’s where we left it.  Coming back to the meta point above though, the core team really doesn’t want to discuss this right now, given that this is purely a sugar proposal and we need to stay focused on the primary Swift 3 goals.  If Matthew or another community member wants to pick this up and keep running with it, that is perfectly fine, but we should defer more formal discussion about the proposal until at least march/april-ish.  Likewise, other pure sugar proposals should similarly be deferred at this point, unless they have some strong motivation that aligns with a Swift 3 goal. 
> 
> I want to thank Matthew in particular for passionately pushing this area forward!
> 
> -Chris
> 
> 
> 
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160114/2943fdd7/attachment.html>


More information about the swift-evolution mailing list