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

Matthew Johnson matthew at anandabits.com
Wed Dec 23 11:25:30 CST 2015




> On Dec 22, 2015, at 1:29 PM, Chris Lattner <clattner at apple.com> wrote:
> 
> 
>> On Dec 22, 2015, at 8:46 AM, Matthew Johnson <matthew at anandabits.com <mailto: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:

Excellent, thanks!  I have completed a third draft of the proposal.  It may (probably does) still require further refinement but I believe it is another solid step forward.

Here’s the complete draft: 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>

Here’s the commit diff in case that is more helpful: https://github.com/anandabits/swift-evolution/commit/8287b67569038000aa4231cc7725e5fbeb7fe8ce?short_path=f5ec377#diff-f5ec377f4782587684c5732547456b70 <https://github.com/anandabits/swift-evolution/commit/8287b67569038000aa4231cc7725e5fbeb7fe8ce?short_path=f5ec377#diff-f5ec377f4782587684c5732547456b70>

Discussion on a couple of topics continues inline below as well.

> 
>>  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.

Maybe it wasn’t clear, but I was suggesting that this particular rule be changed for all initializers.  That would have made it possible to write the initializer out longhand.  But I have pulled that part of the proposal so it doesn’t matter now. 

> 
> 
> 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.

I would have never considered writing a type with an instance member with constant value that cannot be assigned a different value in the initializer as I would have considered it wasted space and possibly worthy of a compiler error.  I would have written:

public class X {
  static let a = 42
}

Clearly I was underestimating the optimizer!  I can see value in the ability to refer to the member without a prefix.  Given the (guaranteed?) optimization it definitely makes sense.  

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

As you said you agree with the desire to support this behavior, maybe you would be comfortable with a different approach.  Rather than using the initial value we could use an attribute:

public class X {
  @default(42) let a
}

This would allow us to still support the desired memberwise initialization behavior without conflicting with the current “initial value” behavior.  I’m have updated the proposal to specify the behavior this way.  If you don’t like it let’s continue the discussion.

It could be called @default, @memberdefault, @memberwisedefault, etc.  The name doesn’t really matter to me.


It is irrelevant to the current proposal, but this attribute might also be useful in longhand cases allowing the “magic value” to be specified in a single location:

struct S {
  let  value: Int?
}

public class X {
  @default(42) let a     // same potential utility if this is a var

   // default is a keyword that is only valid in an expression
   // on the rhs of a member initialization statement
   // or possibly an initializer parameter with a name corresponding to a stored property

   init(s: S) {
      self.s = s.value ?? default    
   }
   init(int a = default) { … }
}

As an alternative to specifying a value directly in the attribute, it could require a `let` member name, although my initial reaction is that this is more verbose and less good:

public class X {
   let defaultForA = 42
  @default(defaultForA) let a
}


> 
> 
>>> 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’m glad I found an alternative that you like better. :)

> 
>> 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.

That makes sense.  I have updated the proposal to reflect this.  Incremental change, right? :)

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

I absolutely agree that the compiler should not be in the business of making a guess about what superclass initializer needs to be called!  

This was actually a mistake in the proposal and isn’t what I intended.  I’m not sure how I missed it and am embarrassed by the oversight!  The intention is to require subclasses to make a call to the superclass initializer that is unambiguous on its own.  So the example as it exists should produce a compiler error just like you thought!  

The example was intended to include a call to provide all of the arguments necessary to disambiguate prior to memberwise argument forwarding.  In this case of the present example none are necessary.  I am including the corrected example here and have also updated the proposal.  It should have read like this:

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(...) { super.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/tree/flexible-memberwise-initialization#detailed-design>

This is obviously a trivial example and far more complex scenarios are possible.  Are you willing to continue exploring inheritance support in the manner I intended, where no guesswork by the compiler is necessary?  The solution would need to make everything unambiguous in user code alone.  

I understand there also potentially resilience concerns around supporting inheritance.  If you’re willing to dive into specifics I may (or may not) be able to find solutions.  If I am not able to identify solutions, at least I have tried and understand the specific issues involved.  :)

If you’re willing to do this I would be happy to work through any scenarios necessary regardless of how complex they get.  :)

I personally don’t like inheritance very much and consider it a tool of last resort, but I do think it is best for a feature like flexible memberwise initialization in a language that offers inheritance should support it if at all possible.



> 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.


Ideally we could avoid writing this manually.  We still have an M x N problem (M inherited properties, N subclass initializers).  

Writing that manually would’t reduce the utility of the feature in cases where it is applicable, but it does severely limit its applicability to inheritance hierarchies.


> 
> Thank you again for pushing this forward!
> 
> -Chris


You’re welcome!  I know this proposal is “just” syntactic sugar, but I believe it will have an impact on initialization designs in practice.

I really appreciate the time you’re taking to get into the details and help refine the proposal!  

Matthew

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


More information about the swift-evolution mailing list