Explicit Memberwise Initializers

#4

Thanks for the pitch, it has a lot of interesting ideas. If we're just considering memberwise initialisers though, and not property packs as a more general feature, then this seems like a lot of complexity and new syntax. My main issue with having to write out initialisers in full is that they require a lot of repetition. In the common case you have to mention each property name three times and duplicate the types as well. But I also don't necessarily need or want to eliminate all sources of repetition because it can become confusing or cryptic (e.g. private... referring to all properties rather than just the private ones).

While reading this an alternative idea came to mind, and from the rationale link it looks like @owensd had the same idea back in 2016. If you just allow self.x in the initialiser parameters to expand in the “obvious way”, you can replace @Avi's example initialiser by:

init(self.title, self.name, self.code, self.visible, self.dependency,
     self.weekday, self.variables, self.attributes, self.setId) { }

which eliminates a ton of boilerplate up front (the types and two mentions of each property name). This gives you a lot of flexibility in a straightforward way: adding new parameters, reordering parameters, adding default values, changing argument labels, etc. This seems closer to the sweet spot on the brevity/complexity/confusion curve to me.

On top of this simple model you could consider adding something like parameter packs where you can refer to certain subsets of properties, which you can then explain easily as just conceptually expanding to self.property1, self.property2, … in the initialiser list.

2 Likes
(Matthew Johnson) #5

Every feature has to start somewhere. Property packs and expansions could potentially be leveraged in many ways going forward. As an example, I sketched out a future direction showing how they could be useful in reducing boilerplate for any memberwise implementations (such as Equatable and Hashable).

You may have noticed that this idea is present in the new proposal using the x... syntax. The core team felt the self.x syntax was “weird and could be improved”. They also noted the con of this approach that all properties must be listed out in all memberwise initializers. Property packs fill that gap well and provide rationale for a better syntax that is also able to support the individual property use case.

It eliminates some boilerplate but still leaves you repeating the name of each property in each initializer. The proposed design is able to support everything you mention above while also scaling up incrementally as you need to deviate from basic memberwise initialization.

This is exactly what property packs do when they are expanded. Is that not clear in the proposal? Are we describing the same thing and just conceptualizing / explaining it in different ways?

#6

Sure, I don't find x... to be less weird than self.x though. It is considerably more weird to me, both being an entirely new syntax (rather than reusing a standard way of referring to properties) and having ellipses that suggest it refers to something more than just x when it doesn't. And it gets even weirder when you start having to interpret something like init((internal | externalLabel detail2)...). I accept the downside of having to list the properties, but I don't see it as a fatal flaw, just as making a set of tradeoffs on the brevity/complexity/confusion curve.

It's clear, that's why I said “something like parameter packs”, but I didn't want to get into the details here (e.g. private... in your proposal is likely to cause a lot of confusion, I don't think the additional complications around altering elements of the pack are necessary, etc). Perhaps I shouldn't have mentioned this possible expansion at all because I'm not at all convinced that it is necessary; even the simple option of just allowing self.property in argument lists reduces the number of non-whitespace characters in @Avi's example by 65%.

(Tino) #7

Initializers are already a quite complex topic with many special rules, and imho we shouldn’t make them even more complicated.
I‘m not saying there shouldn’t be more convenience in this area, though - but I‘d prefer convenience that is not specific for that narrow use:
If we revive the capability of calling every methods with a single tuple, this might be something to build on - and it would not only help with initializers (think of something like super.overriddenMethod(#params).

5 Likes
(Dan Zheng) #8

Is it possible in this design to declare an explicit memberwise initializer with a body that references the expanded init arguments? I think it's fairly common to "define a memberwise initializer with extra logic" - the extra logic could be preconditions or debug printing.

1 Like
(Matthew Johnson) #9

This syntax is borrowed from parameter pack expansion in variadic generics. The ellipses represents the expansion of a pack. Swift doesn’t support variadic generics yet but many of us hope it will someday. So while this syntax is new to Swift, I think it fits very well with the long term vision.

This syntax represents pack composition followed by expansion of the composed pack. The alternative is to simply list out each property in the necessary order. I think pack composition is a powerful concept that would be useful. That said, it is something that could be moved to a future enhancement (possibly at the core team’s discretion). It isn’t strictly necessary for the initial proposal, although in my experience it is something that will be quite useful.

Reducing boilerplate is a significant part of the purpose of this proposal but I think highlighting the delta from the trivial is also very important. When all parameters have to be listed it is no longer clear whether they match the order of other initializers as well as the property declarations themselves without inspecting all of that code. When each initializer focuses exclusively on the details that deviate from memberwise initialization that information immediately pops to the foreground. This is very useful and gets lost when every initializer is required to explicitly list out every property it is initializing.

(Matthew Johnson) #10

Yes! The body of the initializer is able to directly reference properties that have been initialized by the expansion. Here is an example of this from the proposal:

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)
  }
}
(Pierpaolo Frasa) #11

I like the flexibility that this proposal affords (and we can of course always bikeshed notation, but that's not the immediate point), but in the spirit of progressive disclosure and allowing the most common things to be expressed in the simplest way possible, I'd like to have some more convenience syntax.

First of all, I'd like to be able to leave off the {}, when they don't include anything. Otherwise it's just noise. Second, I'm thinking of being able to leave off the (...) part after init when it would refer to private (or maybe "whatever the visibility of the struct is"?). That way, you could write things like:

public struct PublicStruct {
    let x, y: String
    public init
}

internal struct InternalStruct {
    let x, y: String
    private init
}

etc. This would already cover a lot of use cases (not losing initializers with public structs, being able to declare initializer visibility separately from type visibility) and the syntax would be intuitive enough for many people (I believe), while being straightforwardly compatible with the more advanced notation used for more special use cases. This way, the more complicated initializer syntax only needs to be learned (and parsed!) when there are more special use cases.

1 Like
(Caleb Kleveter) #12

It seems to me that this is part of the larger 'modify synthesized code' issue. This usually occurs when someone realizes that the synthesized initializers or types (such as the CodingKeys enum) are internal, so they try to find a way to make them public without manually defining the whole thing.

So what if instead we added a @synthezied attribute or keyword that could be added to a type or method's signature. This would allow you to define the actual signature of the method/type, but the implementation would be synthesized for you:

@synthesied public enum CodingKeys: String, CodingKey, Hashable, Codable

We could add the property packs as suggested in the proposal and use that for memberwise inits:

@synthesized public init(internal...)
#13

My top priorities in this area are being able to:

  1. Retain the memberwise init when other initializers are present.

  2. Exclude certain properties from the memberwise initializer.

  3. Specify the access level of the memberwise initializer.

I’m going to need some time to digest the entire proposal. As an initial impression, it is not clear to me that “grouping properties by visibility” is either necessary or sufficient for these purposes. I recognize there is power in the proposed parameter packs, however I am not convinced they are the best tool for this job.

There has in the past been some discussion about marking individual properties as “transient” or “nonsalient”, to exempt them both from participation in the memberwise initializer, and from synthesized conformances to Equatable, Hashable, and Codable.

I tend to lean in favor of that approach, as generally speaking all four of those features should involve every stored-property instance member, with very few exceptions for things like caches.

It might make sense to start with a small targeted proposal for just #1 and #3, to allow concise explicit declaration of the memberwise initializer including its access level, and defer #2 (exempting individual properties) to a future proposal that also covers the compiler-synthesized protocol conformances.

• • •

I think the proposal would benefit by surveying Swift projects such as those in the compatibility suite, to identify:

• How often the explicit memberwise initializer could be used
• How many of those would use the simple form
• How many would just change the access level
• How many would be able to use a single access-level parameter pack directly
• How many would exempt just one single stored property

If it turns out that most uses would be simple, or just change the access level, then I think the current proposal’s complexity would be disproportionate to its benefits.

(Matthew Johnson) #14

This syntax looks nice but I don’t think this omitting the property pack is viable. I would be opposed to the compiler implicitly exposing any properties less visible than the initializer. We would need to produce a compiler error in these cases which disallows this sugar. If we can’t use it when we want to elevate the access level of the implicit memberwise initializer it won’t be usable in enough contexts to carry its weight.

We could allow omitting an empty body but we don’t do that anywhere else in Swift and it’s only two characters. Further, it communicates that the body is intentionally empty. I think omitting empty bodies should be a separate pitch and address more than just initializers if you want to go in that direction.

(Pierpaolo Frasa) #15

Well, alternatively it can also default the property pack to the same visibility as the initializer, if you think that would be a better default.

I disagree on several points. Firstly, this is not a standard feature in Swift, so it's hard to compare it to the rest of the language; this feature is, fundamentally, something along the lines of metaprogramming. If we think about it in these terms, a method body that can be left off is not too dissimilar to a closure that defaults to an empty closure, e.g. something like

func generateInit(
    propertyPacks: [PropertyPack] = defaultPropertyPacks,
    afterInit: () -> () = {}
) {
    memberwiseInit(propertyPacks)
    afterInit()
}

but at compile-time. I don't think this is, in any way, confusing.

Second, I'm not sure that accepting a proposal such as this one without some more convenient syntax for more common use cases, is a good addition to the Swift language. The situation around memberwise is very disappointing, because when you start developing everything looks really neat, but once you change even just the visibility of a struct, you lose all the benefits. Requiring everyone to learn about such a complicated feature as property packs just to change the visibility of the memberwise initialiser is, I think, too high of a price to pay. This feature should probably still exist, but it's probably much more useful to people who have more complicated requirements (e.g. framework authors) and can be expected to learn about such things.

1 Like
(Matthew Johnson) #16

The problem with this is that the type might have properties with lower visibility than the initializer. As I said in my previous reply:

We would need to produce a compiler error in these cases which disallows this sugar. If we can’t use it when we want to elevate the access level of the implicit memberwise initializer it won’t be usable in enough contexts to carry its weight.

It’s nice syntax but is only usable in a very limited context - when the implicit initializer isn’t sufficient but you don’t need to do anything different except for change modifiers without elevating the access level. On the other hand, if we wanted to deprecate the implicit initializer and replace it with this sugar it would very much pay for its weight. But I don’t want to tie fixing the boilerplate problem to a massive breaking change.

That’s fair. To be honest, I’m fairly ambivalent about this. If people want it an the core team wants to include it that’s fine with me. I just don’t view it as super important.

Do you agree that a declaration that exposes a less visible property should have some syntax indicating that it is doing that. I think the syntax I have proposed is about the lightest way syntax possible that still adheres to this principle. You are able to clearly see the difference between the visibility of the initializer and that of the least visible properties it exposes. I think this syntax can be clearly and concisely explained as follows:

As a convenience when direct property initialization is desired, an initializer may specify the lowest access level of the properties to be automatically initialized: init(internal...). When it does this parameters are automatically inserted into the parameter list (in property declaration order). The properties are automatically initialized with the arguments provided by the caller. The ... suffix used in this syntax represents the expansion of the placeholder into parameters and property assignments.

(Pierpaolo Frasa) #17

Tbh, I don't really have an opinion on that, because I often treat structs (and enums) as pure (immutable) algebraic data types, so I don't tend to have types with members that have a different visibility. What I tend to do is change a type from internal to public, or expose a type (publicly or internally), but keep its initialiser private (what you call "limited context"). So maybe someone else can answer this question better.

To be perfectly fair, this description reads to me like something a compiler author would write. It's too much concerned with what the compiler does imperatively than with what the intent of that syntax is and why you would use it.

But again, maybe my mindset (thinking of types in terms of ADTs which you should be able to trivially construct) is very different from people who come e.g. more from an Objective-C background and maybe do tend to prefer to think of explicit initialisation steps instead.

(Matthew Johnson) #18

If this is how your code looks then I can see that this sugar would be valuable to you even with a design that restricted its use to cases where all properties without default values are at least as visible as the initializer itself. If this is common enough this would make a good followup proposal.

I’m sure it can be improved, but I don’t think it’s too bad. The fact is the reason you want to use this syntax is precisely to get the compiler to write code for you.

Yeah, it sounds like this is the case. Swift’s initializer model is an imperative one. Simple memberwise initializers have an analogue in ADTs but they are not limited to this model. In particular, it’s worth noting that enums can have custom initializers in addition to the case initializers that are implicitly provided by the language.

1 Like
#19

Regarding access control:

The existing implicit memberwise initializer has visibility equal to the minimum access level of all stored properties (capped by internal). That will not change.

In some cases, the author of the type would like to give the memberwise initializer a higher access level. They could write such an initializer manually, but it would consist solely and exclusively of the exact boilerplate that we are trying to eliminate here.

It would be much more in line with expectations if they could instead simply mark the difference from basic memberwise initialization. And that difference is…just the visibility of the initializer.

If the author wants the memberwise initializer to be public, even though some stored properties are internal, that is perfectly fine. They should be able to indicate concisely “I want the behavior of the standard memberwise initializer, and I want it to be public.”

In particular, changing the access level of a memberwise initializer should not change its signature or functionality. It should only change the access level.

Also, note that having a higher-visibility memberwise initializer emphatically does not break encapsulation. It does not reveal any hidden properties, it does not give access to them, and indeed there is no indication to clients that the initializer is even operating memberwise. For all anyone on the outside know, it could be a manually-coded initializer that just happens to have certain argument labels and parameter types.

If there is a strong motivation for additionally allowing parameter packs based on the visibility of properties, then that feature can stand on its own. But the core behavior of memberwise initializers should remain as it is, unencumbered by such add-ons.

The most important piece to me is that we should be able to retain the memberwise initializer even when other initializers are added. It is a somewhat lower priority to be able to specify the visibility of the memberwise initializer, however if and when we do introduce such a feature, it should only change the visibility.

An edge case is if some stored property has a type with restricted visibility. But we already have a diagnostic for that, so it isn’t actually a problem:

struct Foo {
  // Initializer must be declared private because its parameter uses a private type
  init(_ x: Bar) {}
  private struct Bar {}
}
4 Likes
#20

This looks nice to me! I share the thought that the {} should be omitted, though. Memberwise initializer syntax should imply a synthesized code block.

(Davide De Franceschi) #21

Isn't this already true whenever the other initialisers are added in an extension?

1 Like
(Matthew Johnson) #22

Yes it is. The change with explicit memberwise initializers is that you are able to put other initializers in the declaration itself if you prefer and are still able to declare the memberwise initializer. So if you don’t want to use an extension you won’t have to.

2 Likes
(Adrian Zubarev) #23

Edit: The idea of explicit memberwise initializers is great. Before I start babbling my thoughts I want to clarify that I did not follow or read the previous threads of this discussion.


The pitched design is quite intuitive, but it has its quirks. When someone new to the language reads (internal...) in an intializer I believe that lots of question will come up:

  • What is this?
  • Why is there an access modifier?
  • Why are there trailing ...?

Personally I think only the first question really matters when you discover this feature, but I also think that the packs should not be that cryptic. I would like you to consider an alternative design that might be a little verbose, but it tries to solve a few more problems by also preserving better discoverability, consistency with other language features and potential re-usage in the future.

The direction of this idea is leaned towards the things the pitch already has mentioned:

  • Prefix implicit property packs with #
  • Different semantics for the implicit property packs

But I would like to take the change and expand or change these things a little.

  • We should use a # prefixed construct for example #colorLiteral.
  • If possible we should not use access modifiers directly as this creates more confusion and does not tell the reader what this construct possible mean.
  • For the simplicity let's call this #member (we can debate on the name later).
  • Such #member(...) construct contains either a pattern, a range or a combination of both things.
  • #member always describes an exact set of access modifiers.
  • A pattern can be composed with | or & to express concatenation or an OR like extraction of members.
#member(private) // will extract only all `private` members from top to bottom

#member(public & private) // will extract first all `public` then `private` members
#member(private & public) // will extract first all `private` then `public` members

#member(private | public) // will traverse from top to bottom an pick any `private` or `public` members
#member(public | private) // same meaning 
  • A range can be expressed by using two access levels with an ... infix operator in between them (only closed ranges should be supported). The order of the access levels does not matter as a range is a simplification of a composition of multiple access levels using |.
// assuming this order:
// private - fileprivate - internal - public - open

#member(private | fileprivate | internal | public) // explained above
#member(private ... public) // same meaning as the example one line above it
#member(public ... private) // order does not matter - same meaning
  • We could also extend the pattern to allow explicit inclusion of members regardless their access level, which also allows us to provide an alternative custom label (the original pitch is already doing that).
// * picks everything from top to bottom that is private ... public
//   and overrides label of `someMember`
// * if `someMember` is not in that range, this is an error
// * if `someMember` is for example `open` the compiler could provide
//   a fix-it and suggest using `&` instead of `|`
#member(private ... public | (label someMember)) 

The extraction of the members prevents generating duplicate entries. That means that if a previous pack or pattern extracted some specific member from the ordered set of members, it won't be added with the next pack expansion that overlaps. This should simplify things as it does not need to check for overlapping patterns.

You should be able to create any possible pattern with this approach with a little trade-of in pattern definition.

// Attention, this is a COMPLEX pattern and not the common case.
public init(
  #member(public),
  label argument: Type, 
  #member(private & (fileprivate | internal))
)

// Assume there is a non-public `argument` member.
// Then we can define the same pattern differently.
public init(#member(public & (label argument) & private & (fileprivate | internal)))

It's likely that in most common cases the user will only need to define ranges such as #member(private ... public). We could go one step deeper and use open ranges to reduce the verbosity.

#member(private...) // we just re-invented what the proposal had as `private...`
#member(private ... open) // basically the same

#member(...internal) // different direction
#member(private ... internal) // same

Overall I think we should cover this feature into a # prefixed construct as it is most likely extendable in the future if would decide to allow it for methods. Then we could add extra constraints:

#member(...public, [.computed, .stored])

To signal the feature more obviously I thin we could call it #property.

1 Like