Explicit Memberwise Initializers

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

(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(
  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
(Adrian Zubarev) #24

Taking original examples and applying my ideas to them:

struct Foo {
  var exposed1: Int
  private var detail1: Int = 0
  var exposed2: Int
  private var detail2: Int = 42
  var exposed3: Int
  private var detail3: Int

  // this initializer:
  init(#property(...internal | externalLabel detail))
  // note: the parameters are in property declaration order
  // expands to:
  init(exposed1: Int, exposed2: Int, externalLabel detail2: Int, exposed3: Int) {
    self.details3 = details3
    self.exposed1 = exposed1
    self.exposed2 = exposed3
    self.exposed3 = exposed3
(Adrian Zubarev) #25

To make it easier to examine how good or bad my idea works feel free to post examples of properties and describe how you want the initializer to behave, I will try to provide examples for patterns that will hopefully cover it and depending on the situation simplify the amount of boilerplate needed using either a simple or a more advances pattern.

Edit: No one has replied yet, I had to edit my post as the limit of sequential replies for the same person is set to 3 posts.

One issue about open ranges and the access... syntax from the original pitch. These things are great on paper, but they hide one 'possible' issue. Since we operate on access levels we can hit a problem in the future. Let's pretend that access levels are not set in stone and Swift finds a way to do something about it in an amazing way that the whole community will love (just pretend that without any comments). So if Swift were about to add a new access levels that is greater, lower than current one, or even an intermediate one that 'could' be a huge breaking change as open ranges and even normal ranges could reach that new access level unintentionally. It is like if the stdlib had a non-frozen enum AccessLevel where this feature would use its cases, ranges will fail if this enum was extended, while explicit access | access annotation will not.

With this I don't want to start any debate about access levels, nor derail that thread, but I want to point this issue out so we keep it in mind when we consider the design.