Explicit Memberwise Initializers

(Matthew Johnson) #1

In the spirit of reviving deferred proposals and improving support for EDSLs (many of which involve heavy use of memberwise initialization) I decided that now would be a good time to revisit SE-0018. I don't have an implementation but I do have what I believe is a very much improved design.

I will maintain an updated copy of the new proposal at this link. I am also including the current draft below to kick off discussion:

Explicit Memberwise Initialization

Introduction

This proposal provides syntax that allows us to explicitly declare the existing implicit memberwise initializer. Once we have explicit (but concise) declaration syntax in hand we are able to modify it in useful ways when we need more control over the synthesized code.

Swift-evolution thread: Discussion thread topic for that proposal

Motivation

SE-0242 added default parameters to the implicit memberwise initializer for structs. This is a wonderful improvement, but even with this step forward the memberwise initializer remains relatively narrow in its scope of applicability. Any need to deviate at all from the standard behavior faces the steep cliff of having to write every detail out manually. It also loses the ability to elide initial value side effects of any var property that the initializer will provide a value for.

This is unfortunate and unnecessary. Instead, there should be a gradual change in syntactic weight that is commensurate with the required change in behavior. While reading the examples below consider how quickly the boilerplate grows for types with many stored properties. Eliminating this tedium is really important: it obscures the useful information contained in the code that describes the difference from basic (and boring) memberwise initialization. This code that describes the difference should be front and center for a reader.

Proposed solution

The proposed design introduces and builds upon stored property packs. A property pack is an ordered set of properties. When a property pack is expanded by a memberwise initializer, the properties in the pack are spliced into the parameter list and the body of the initializer is prefixed with memberwise intiailization of those properties.

  • Note: the name “property pack” was inspired by parameter packs in variadic generics.

Declaring the implicit memberwise initializer

struct Point {
  let x, y: Double

  // this initializer:
  init(internal...) {}

  // expands to:
  init(x: Double, y: Double) {
    self.x = x
    self.y = y
  }
}
struct Foo {
  private let details: Int
  let exposed: String

  // this initializer:
  private init(private...) {}

  // expands to:
  init(details: Int, exposed: String) {
    self.details = details
    self.exposed = exposed
  }
}

Implicit stored property packs are provided for each access level providing a lower bound of visibility for properties included in the pack. Properties placed in the pack in declaration order. This means that the existing memberwise initializer is spelled by expanding the pack matching the lowest access level used by a stored property of the type. In order to match the synthesized initializer, the explicitly written initializer must also use that access level.

Implicit property packs are based on access levels in order to make it very clear in the initializer declaration the degree to which pack expansion may expose implementation details. This design also supports the intended use case of providing memberwise initialization for more visible properties while using a different initialization strategy for private implementation details.

Mixing memberwise and manual initialization

One example of a stored property that adopts a different initialization strategy is a cache. A cache may be initialized with a default value or an initial value computed using other members stored in the type. In either case today we are required to write an entirely manual initializer.

struct Foo {
  private var cache: [String: Int] = [:]
  let exposed1: String = "hello"
  let exposed2: String = "world"
  
  // this initializer:
  init(internal...) {}

  // note: default arguments are provided as per SE-0242
  // expands to:
  init(exposed1: String = "hello", exposed2: String = "world") {
    // note: initial value assignment of cache is not suppressed so `cache = [:]`
    self.exposed1 = exposed1
    self.exposed2 = exposed2
  }
}

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

Increasing and lowering visibility

Now that we are able to state the memberwise initializer explicitly we are also able to raise the visibility of an initializer that expands a pack.

struct Foo {
  private let details: Int
  let exposed: String

  // this initializer:
  init(private...) {}

  // visible internally despite exposing initialization of a private property
  // expands to:
  init(details: Int, exposed: String) {
    self.details = details
    self.expose = exposed
  }
}

We are also able to use the access level of setters for var properties instead of getters (the implicit memberwise initializer uses getter visibility).

struct Foo {
  var internalProp: Int
  private(set) var privateProp: Int = 42

  // this initializer:
  init(internal(set)...) {}
  
  // privateProp is omitted because its setter is private
  // and the pack specified `internal(set)` as the minimum setter visibility
  // expands to:
  init(internalProp: Int) {
    // note: initial value assignment of privateProp is not suppressed so `privateProp = 42`
    self.internalProp = internalProp
  }
}

public memberwise initializers are now possible as well.

public struct Foo {
  private let details: Int
  public let exposed: String

  // this initializer:
  public init(private...) {}

  // visible internally despite exposing initialization of a private property
  // expands to:
  public init(details: Int, exposed: String) {
    self.details = details
    self.exposed = exposed
  }
}

Modifing parameter order and external parameter labels

Implicit single-property packs are also provided for each stored property. This can be used to modify the external labels and order of properties in the initializer. Library authors in particular will find this valuable as it provides some sugar while providing explicit control over the exact signature of a public API.

public struct Foo {
  private let details: Int = 42
  public let exposed: String = "hello"

  // this provides an explicit order that is *different* from
  // the property declaration order and modifies the label for details
  // this initializer:
  public init(exposed..., labelForDetails details...) {}

  // expands to:
  public init(exposed: String = "hello", labelForDetails details: Int = 42) {
    self.exposed = exposed
    self.details = details
  }
}

Property packs may be composed before expansion. Pack composition ensures all of the expanded parameters appear in the order of declaration of the expanded properties.

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((internal | externalLabel detail2)...)
  
  // 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
  }
}

If we had declared the above example without pack expansion and instead just placed detail2... before or after internal... in the parameter list detail2 would appear first or last respectively in the parameter list. Instead, with pack composition, it appears in property declaration order as desired.

This supports use cases where property declaration order is the "source of truth" and packs are expanded in several different memberwise initializers. Without pack composition the parameter order for each initializer would need to be maintained manually and kept in sync with all of the other initializers and the property declaration order.

Note: the use of | for pack composition is tentative. I am open to suggestions.

let properties

By default, a let property is omitted from all parameter packs if its declaration assigns a value. When the assigned value is intended to be a default while still allowing other values during initailization, the @initializable attribute may be used.

struct Foo {
  let timeout = 42
  @initializable let name: String = "first"

  // this initializer:
  init(internal...) {}

  // expands to:
  init(name: String = "first") {
    self.name = name
  }
}

Classes

Designated intializers of a class may use property packs in all the same ways as struct initializers, however because pack expansion immedatiely assigns to properties only packs of properties declared by the class itself are supported. Properties declared by superclasses are not supported. The body of the initializer is responsibly for forwarding any necessary parameters to the superclass initializer.

This proposal does not introduce an implicit memberwise initializers for classes. If experience with explicit memberwise initialization demonstrates that this would be beneficial it can easily be added later.

Detailed design

The crucial design decision in this proposal is to adopt the approach of packs and expansions.

Packs and expansions

Property packs share with variadic generics the notion of a "pack" and an "expansion". When it is applicable, the approach of packs and expansions provides an extremely concise and declarative approach to static metaprogramming. It is simpler and less error prone than more powerful approaches such as hygienic macros. Even better, when expansion alone is not enough these approaches can work in combination: one can imagine a macro using static reflection to analyze a type and decide which pack to expand while performing code synthesis by expanding that pack (rather than performing the expansion itself).

Property packs

Property packs are a foundational construct that could support features well beyond memberwise initialization. For example, property packs could be considered as an alternative to user-defined attributes in specifying which properties participate in Equatable, Hashable and Codable conformances (of course the pros and cons of each approach would need to be weighed carefully). This future direction is covered at the end of the document.

Explicit memberwise initialization provide a relatively straightforward and common use case in wheich to familiarize Swift programmers with the notions of packs and expansions. This should help to make other powerful language features built on packs and expansions (such as variadic generics) feel more accessible than they otherwise might.

For the purposes of this proposal, property packs are purely compile-time constructs with no runtime representation.

Implicit property packs

As any other identifier, a lexical context may define implicit property packs. This is analogous to newValue in setters, oldValue in property observers and the $ identifiers in closures. The implicit access level property pack identifiers are defined contextually for struct and class initializers.

The implicit individual property packs specified by this proposal may be defined this way as well. An alternative design would specify that an individual property pack is always defined for every property. Under this alternative we would need the ability to distinguish stored properties from computed properties. For example, only stored property packs should be supported by initializers, but if the future direction of allowing property packs in method signatures is pursued it may make sense to allow computed properties with setters as well, but we cannot support packs including let properties (which are supported in initializers). We should leave this unspecified until we have the opportunity to consider additional use cases for property packs.

Access control

Access control rules for the implicit property packs are applied as one would expect. An initializer declared in a different file may not use the implicit private or fileprivate property packs. An initializer declared in a different module may not use those and also may not use the implicit internal property pack. Availability of individual property packs depend on the visibility of the property itself. In other words, if you cannot write the initializer manually you cannot write it using the implicit property packs.

Grammar changes

TODO

Source compatibility

This proposal is purely additive.

With one exception an initializer that uses pack expansions is identical to writing the equivalent initializer manually. The one exception is that it will not be possible to manually write an initializer that suppresses initial value assignment without using pack expansion. The ability to manually implement these initializers with identical behavior to the form that uses pack expansions is discussed as a future direction below.

Effect on ABI stability

As a syntactic sugar proposal there is no impact on ABI stability.

Effect on API resilience

As a syntactic sugar proposal there is no impact on API resilience.

Alternatives considered

Prefix implicit property packs with #

As with other compile-time constructs, we could spell the implicit property packs #private, #internal, etc (#private... and #internal... respectively when expanded). This may well be the best design. It should be considered carefully.

Introduce a memberwise keyword to mark memberwise initializers

We could restrict the usage of property packs to initializers marked as memberwise. This adds boilerplate and complexity where none is necessary. Introducing the notion of property packs that can be used by any initializer is a simpler, more general model.

Different semantics for the implicit property packs

We could define the implicit property packs to include only the stored properties at the exact access level as the identifier used to name the pack. Pack composition would be used when properties from multiple access levels need to be expanded: (public | internal).... This approach was not chose for several reasons:

  • this approach will be more verbose in common scenarios
  • listing out higher access levels is less important than highlighting the lowest access level that is being expanded
  • the implicit property packs already exclude computed properties and all superclass properties
  • the implicit property packs are only available in initializer signatures so their semantics should be tailored to anticipated needs in this context
  • this design aligns the name of the property pack with the visibility of the current memberwise initializer (which can now be written explicitly using the pack)

There are very good reasons to choose this set of semantics.

Adopt an entirely different approach

The core team analyzed many approaches to memberwise initialization in the rationale provided when SE-0018 was deferred. All of the approaches and analysis described in that document were considered and informed the present design.

Following the review of SE-0018 I also provided an analysis that explored some new directions that were discussed during and inspired by the review of SE-0018. These approaches were also considered and informed the present design.

Future directions

Explicit property packs

We could allow programmers to explicitly create property packs by using an @pack(packName1, packName2) attribute on properties. This could be useful in cases where several initializers need to expose the same group of memberwise parameters but they don't fall together into one of the implicit property packs.

Initial value assingment suppression

Property pack expansion replaces the initial value assignment for any @initializable let or var property included in the expansion. We could extend the ability to replace initial value assignment to any initializer (including memberwise initializers that want to explicitly initialize one of these properties instead of using a property pack). This could be supported using @initializes(property1, property2).

In addition to being useful, this would reduce the magic involved in memberwise initialization by providing syntax that allows us to explicitly write out the expansion of a memberwise initializer if desired.

Support property packs in method signatures

We could allow property packs of var properties with setters (including computed properties with setters) to be expanded in method signatures. This is an interesting direction to consider. In some sense it would provide symmetry with initializers but as noted in the detailed design, the rules about what expansions are supported would need to be somewhat different.

Support property packs in other contexts

As with initialization, property pack expansion could reduce the steep cliff from synthesis to a full manual implementation in many areas. For example, it may be interesting to explore a direction that supports writing a wide range of memberwise expression or statement chains using property pack expansions.

public static func == (_ lhs: Self, _ rhs: Self) -> Bool {
   return (lhs.#packIdentifier == rhs.#packIdentifier &&)...
}
public func hash(into hasher: inout Hasher) {
   hasher.combine(#packIdentifer)...
}

Note: the syntax in the above example is a strawman only used to demonstrate the idea, it is not intended to represent a well thought out design.

4 Likes
SE-0257: Eliding commas from multiline expression lists
#2

I am definitely in favor of this proposal. Here's a motivating example from one of my codebases:

init(row: Row) throws {
    self.id_k = row[0]
    self.title = row[1]
    self.name = row[2]
    self.code = row[3]
    self.visible = row[4]
    self.dependency = row[5]
    self.weekday = row[6]
    self.variables = [:]
    self.attributes = [:]
}

init(title: String,
     name: String,
     code: String,
     visible: Bool,
     dependency: String,
     weekday: Int,
     variables: [String : String],
     attributes: [String : String],
     setId: Int? = nil) {
    self.title = title
    self.name = name
    self.code = code
    self.visible = visible
    self.dependency = dependency
    self.weekday = weekday
    self.variables = variables
    self.attributes = attributes
    self.set_id = setId
}

As can be seen, the second initializer is written out because it is suppressed by the explicit initializer which is in place to satisfy a protocol requirement. There's no reason the compiler shouldn't be able to synthesize it for me, as it would if the first init was absent.

2 Likes
(Dan Zheng) #3

One cool thing about explicit memberwise initializer declarations is that they can be marked with custom attributes. Use cases:

@usableFromInline init(internal...) {}
@objc init(internal...) {}
...
#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.