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

Chris Lattner clattner at apple.com
Tue Dec 22 13:29:32 CST 2015

> On Dec 22, 2015, at 8:46 AM, Matthew Johnson <matthew at anandabits.com> wrote:
> Hi Chris,
> I have given your feedback a lot of thought and have taken another run at this proposal from a slightly different angle.  I believe it is a significant improvement.  

Hi Matthew,

I continue to really like the approach and direction.  Here’s an attempt to respond to both of your responses, I hope this comes across somewhat coherent:

>  I hope you’re willing to entertain on some discussion on some aspects of the proposal that you are not immediately sold on.  :)

Yes, absolutely.

>> In the case of let properties, I’m uncomfortable with this behavior and it contradicts our current init rules (the synthesized code isn’t legal).  Please change the example to var properties, and then it’s can fit with the model :-).  
> I understand that this is not legal under the current init rules because the compiler currently produces a synthesized initialization of the properties to their initial values at the beginning of the initializer.  I actually don’t like this behavior because it doesn’t allow an initializer body to initialize a let property with an "initial value”.  
> I’m pretty sure I’m not alone in this.  People want to use immutable properties with “default values” (I believe this is what most Swift developers are calling what the Swift team refers to as “initial values”) without a requirement that all instances actually have that value.  It was actually pretty surprising to me, and I’m sure to others as well, to discover this limitation.  I actually thought it was a limitation of the current implementation rather than something that was intentionally designed.  I’m surprised to hear otherwise.

I completely agree with your desire to support this, and I’m sure that if we did, that a ton of people would use it and love it.  However, I really don’t think this is a good idea.

There are two major problems:

Problem 1: supporting this would *prevent* us from allowing memberwise initializers to be public.  A really important feature of the memberwise design is that it is “just sugar” and that people can write the initializer out long hand if needed (e.g. if they want to add or reorder members without breaking API).  With your proposed design, I could write:

public class X {
  let a = 42
  public memberwise init(...) {}

and use it with: X(a: 17).  However, there is no way in swift to write that initializer out longhand.  This is a critical problem to me.

Problem 2: This can cause very surprising performance issues, because it forces the let property to be stored.  With the previous example, it is a goal for us to be able to compile:

public class X {
  let a = 42

into the equivalent of:

public class X {
  var a : Int { return 42 }

because people like to use local lets as manifest constants (avoiding “magic numbers”).  With your proposal, we’d lose this capability, and we’d have to store them any time there is a memberwise initializer.

Neither of these problems apply to vars, which is why I think we can support vars in this model, but not lets.

>> That said, I think the interaction of explicit initializers and memberwise initializers begs discussion.  It would be a much simpler model to only get memberwise parameters for properties without an explicit init.  Have you considered this model, what are the tradeoffs with allowing vars to overwrite them?  Allowing an explicit init to be overwritten by the memberwise initializer seems potentially really confusing, and since you allow explicit arguments on inits, this could always be handled manually if someone really really wanted it.  For example, they could write:
>> memberwise init(s : String) {
>>   self.s = s
>> }
>> If they wanted to get the sugar of memberwise inits (presumably for other properties without an explicit init) but still allow one to be overwritten.
> Personally, I think there is a lot of value in allowing memberwise initialization for properties that do contain an initial value.  Imagine a hypothetical Swift version of Cocoa Touch.  UILabel might have initial values for text, font, textColor, etc but still want to allow clients to provide memberwise initialization arguments to override the default value.  I think there are many cases like this both in UI code and elsewhere.  

I think I confused the issue.  If we have to support properties that have a default value, then the model I’m advocating for is that this:

class C {
  let x : Int
  var y : Int = foo()

  memberwise init(...) {}

compile into:

init(x : Int, y : Int = foo()) {
  self.x = x
  self.y = y

Pertinent points of this are that lets without a default value would still turn into arguments, and that any side effects of the var initializer would be squished.  Another potential model is to compile it to:

init(x : Int, y : Int) {
  self.x = x
  self.y = foo()

which is guaranteed to run the side effect, but requires y to be specified.  I do not think it is a good model to compile it to:

init(x : Int, y : Int? = nil) {
  self.x = x
  self.y = y ?? foo()

because that would allow passing in an Int? as the argument.  The final model (which I know you don’t like) is for memberwise initializers to *only* apply to properties without a default value.

>> This doesn’t seem like the right behavior to me.  The compiler shouldn’t be in the business of scanning the body of the init to decide what members are explicitly initialized.  I’d suggest that the model simply be that the contents of the {} on a memberwise init get injected right after the memberwise initializations that are done.  This mirrors how properties with default values work.
> The model does inject the synthesized memberwise initialization just prior to the body of the initializer.  
> As written the proposal does scan the body of the init in order to determine which properties receive memberwise initialization.  The idea here is that it provides additional flexibility as it allows specific initializers to “opt-out” of memberwise initialization synthesis for some properties while receiving it for others.

This is a very problematic model for me, because it can lead to serious surprises in behavior, and in the case of lets, shares the problems above with not allowing one to define the long-hand form explicitly.

> I don’t think the proposal changes lazy properties. 

I agree, I was saying that I like that :)

>> @nomemberwise is an interesting extension, but since it is a pure extension over the basic model, I’d suggest moving this into a “possible future extensions” section.  The proposal doesn’t need this feature to stand on its own. 
> Allowing type authors to restrict memberwise initialization to a subset of properties is an important aspect of “flexible” memberwise initialization IMO.  In my mind, this is about allowing the author of a type to segment properties that are “user configurable” from properties that are implementation details.  

I understand, but it is a pure extension to the basic proposal.  The proposal is complex enough as it is, so inessential parts should be split out for discussion and implementation.  I’m not saying that we shouldn’t do @nomemberwise in (e.g.) the swift 3 timeframe, I’m saying that it should be a separate discussion informed by the design and implementation process of the base proposal.

>> This also seems unclear to me.  We’re generally very concerned about tightly coupling derived classes to their bases (in an API evolution scenario, the two classes may be in different modules owned by different clients).  Further, the base class may have multiple inits, and it wouldn’t be clear which one to get the arguments from.
> I know there are a lot of complexities here.  It is certainly possible I am missing some showstoppers, especially related to resilience, etc.
> User code would of course need to provide sufficient parameters to disambiguate the call to the base class initializer.
> The goal in this section is to enable memberwise initialization to be used in a class hierarchy.  I think UIKit is a good case to consider for the sake of discussion.  In a hypothetical Swift version of UIKit we would want to allow memberwise initialization of appearance attributes regardless of where they are declared in the class hierarchy.  (I would hope a designed-for-Swift UIKit would not rely so heavily on inheritance, but nevertheless I think it makes good case study for discussing flexible memberwise initialization).
> The same mechanism that handles inheritance should also be able to handle delegating and convenience initializers.  The idea is to write a delegating or convenience initializer that wraps the non-memberwise portion of a memberwise initializer while still allowing the memberwise initialization to “flow through” and be visible to callers of the delagating / convenience initializer.
> I attempted to outline a basic strategy for handling propagation of memeberwise initialization through the inheritance hierarchy as well as other cases of initializer delegation in the detailed design.  It is definitely not complete, almost certainly has flaws and omissions, etc.  I’m hoping we can flesh out the details through community discussion.
> I hope you will agree that it is important to support inheritable memberwise initialization and that we do need to address this in some way.

I don’t agree.  memberwise is a sugar feature intended to handle the most common scenarios.  One of the major reasons we don’t support memberwise inits in classes today is that we have no ability to know what superclass init to call (and root classes aren’t interesting enough to provide a solution that only works for them).  

Given that your proposal allows user code to be added to the synthesized init, I’d really strongly prefer that this be a compile time error, because super.init was never invoked (ok ok, if the superclass has an "init()” as its only DI then yes, we can synthesize it by default like we do today).

>>> 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)
>>>     }
>>> }

Instead, the user should have to write:

memberwise init(baseProperty : Int, ...) {
  super.init(baseProperty: baseProperty)

This doesn’t reduce the utility of this feature, and it preserves separability of class from superclass.

Thank you again for pushing this forward!


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

More information about the swift-evolution mailing list