A new keyword that stands in for a tedious initializer of arbitrary length

I'm not suggesting that we'd do that here; I'm just saying that if the goal is consistency with synthesized conformances then we've already rejected @synthesized but considered @memberwise.

2 Likes

Oh - I was liking the idea actually

Despite a general prejudice against adding language features that merely avoid boilerplate, I would love more support for tooling to generate those initializers. The shortcut in Xcode is a good step, but doesn't work often enough for me. There's an interesting debate to be had about IDE vs compiler support here, too.

2 Likes

Better tooling to auto gen is much simpler than adding yet another keyword. We need less keywords and attributes. My brain need more free space for creativity, not to memorize.

2 Likes

Like Jarod, I’m concerned about how this generalizes to situations where one wants additional parameters, or all members except one, or….

It’s a sticky problem that may have no good solution, but I would be interested in exploring a design direction where:

  • There is some sort of “splatting” syntax that can unpack tuples into individual arguments at call sites.
  • There is also a type splatting syntax for unpacking tuple types in method signatures.
  • There are compiler-generated static types that are aliases for tuple types in the shape of
    1. a particular type’s stored properties and
    2. a particular function’s signature.
  • There are ways of extracting as tuple values of
    1. a struct’s properties and
    2. a function’s arguments.

So for example, ignoring questions of precise naming and syntax, I imagine something of this general spirit:


struct S {
  var x: Int
  var y: String
}

let z: S.Properties  // equivalent to `let z: (x: Int, y: String)`
let zIntoS = S(★z)   // equivalent to `let zIntoS = S(x: z.x, y: z.y)`
                     // (★ is the hypothetical splat syntax;
                     // means “expand tuple into multiple values”)

extension S {
  init(
    ✪stuff: S.Properties,  // ✪ causes separate args at call site to be packed into tuple
                           // In general, ✪ is hypothetical “pack into tuple” syntax
    bonusValue: String
  ) {
    self.✪properties = stuff  // ✪properties is placeholder for magic syntax;
                              // means “all properties as tuple, can be an L-value”
    doBonusStuff(bonusValue)
  }
}

S(x: 3, y: "hello", bonusValue: "frongle")  // works

And similarly for function params:

func f(a: Foo, b: Bar, c: Bazzle) {
  let stuff = ✪parameters  // equivalent to `let stuff = (a: a, b: b, c: c)`
}

func g(a: Foo, b: Bar) {
  f(✶✪parameters, c: defaultBazzle)  // works
}

let gargs: g.ParameterTypes  // equivalent to `let gargs: (a: Foo, b: Bar)`

And maybe even:

func passThrough<Params,Return>(
  _ otherFunc: (✶Params) -> Return
  ✪ params: Params
) -> Return {
  otherFunc(✶params)
}

passThrough(f, a: foo, b: bar)  // works

Thus the OP’s use case becomes the slightly more complicated but far more generalizable:

struct MyStruct <T> {
    private var foo: String
    private var bar: Bool?
    private var baz: Array<T>
    
    public init(✪ properties: Properties) {
        self.✪properties = properties
    }
}

This line of thinking gets messy fast:

  • What about properties with different access modifiers? Are there multiple syntaxes to select for them?
  • What about “all properties except x and y?” Are we heading for something even more esoteric than Typescript’s mapped types?
  • Handling labels gets messy, probably
  • Type checking semantics and performance will be headaches, probably
  • Syntax will definitely be a headache

Still, it’s a direction I’d be curious to explore.

1 Like

If we’re all just tossing out ideas, how about init { default }? This generalizes more readily than memberwise.

1 Like

Xcode has a built-in action for generating those initialisers. Other editors should be able support this, as well (I don't know whether they do right now, but I don't see any reason why they couldn't, since Xcode clearly is able to).

I think this is what @David_Ungar2 is referring to, but it's unclear to me whether OP knows about it. In my experience it has always been very reliable; do you have any examples of types which it fails to show the prompt for?

image

Since this is a purely tooling-based solution, the result is a standard initialiser - you can make it public, @inlinable, add default values to parameters, etc. It involves no extensions to the language whatsoever - and without a new kind of initialiser, nobody will ever have to wonder if a memberwise init comes with any special behaviour compared to a regular init.

Also, consider what happens if I reorder the stored properties of a type - if the initialiser is synthesised during compilation, all code which calls that initialiser would suddenly break (the compiler surely would generate those initialisers as taking parameters in declaration-order); with a generated initialiser spelled out in code, I can reorder the properties without changing the interface of the initialiser. Similarly, I can even rename the stored properties without changing the initialiser. All of that makes it easier for me to edit this code and find the best names for things, without having the entire project fail to compile every time.

3 Likes

The tooling-based approach is great when you want to avoid implicit changes to your interface, but it lacks some of the benefits of the synthesis-based approach.

If I rename a property, then I'll almost certainly want its associated parameter to be renamed along with it. I will pretty much always want the initializer to remain consistent with the declared properties, and I get that automatically with a synthesized initializer. It becomes more tedious to iterate on the model if I previously generated the initializer since I have to constantly update it.

Another benefit of synthesis is the reduction of space taken up by the initializer. A plain memberwise initializer doesn't do anything interesting but takes up quite a bit of space on the page. Many of the types that I use or would like to use initializer synthesis for are little data types that are part of a larger system and are generally nested inside another type alongside other code. When it's just a simple struct with a few properties that's no problem, but if I have to write out an entire memberwise initializer, it becomes too bloated to live alongside other code, and I have to move the type somewhere else away from the code that it's relevant to.

Generating and synthesizing each have their own tradeoffs and use cases. We already support both, and I think it makes sense to continue supporting and iterating on both.

6 Likes

IMO this would make it far too easy to accidentally create source-breaking and ABI-breaking changes to a library.

I would rather the compiler be given the ability to automatically generate code for memberwise initializers. For example:

public struct Circle {
    public var x, y: Double
    public var radius: Double
    
    public init(x: Double, y: Double, radius: Double)
    // automatically generates `{ self.x = x; self.y = y; self.radians = radians }`
}

This would eliminate boilerplate while ensuring that, if the programmer changes one of of the type’s properties, the initializer won’t silently break.

public struct Circle {
    public var x, y: Double
    public var diameter: Double
    
    public init(x: Double, y: Double, radius: Double)
    // error!
}
2 Likes

I'd say this:

struct Circle {
    var x, y: Double
    var radius: Double = 1
    
    init(x, y, radius) // the "member-wise" init we know and love
    init(y, x, radius) // with parameters reordered
    init(x, y) // omitting default parameters
    init(x, y = 2, radius = 3) // with new defaults added
    init(a x, b y, radius) // with parameters renamed
    init(_ x, _ y, radius) // with parameters renamed

    init(x, y, radius) { // with added logic when needed
        self.radius = radius * 2
    }
    init(x, y, radius: Int, somethingElse: T) { // with added logic when needed
        self.radius = Double(radius)
        ...
    }
}
2 Likes

It’s reasonable to consider limiting (some aspects of) this feature to non-stable ABIs (e.g. internal types).

This doesn’t do much to prevent ABI breakage. Reordering, adding, and removing arguments are all ABI-breaking changes that are not caught or impeded by implicitly generating the memberwise assignments.

2 Likes

How would the parser fare with these bodyless initializers? Currently, the compiler can assume that all function declarations within a class, struct, actor, or extension are followed by a body.

To me this seems like the clear syntax winner so far, unless it introduces some unacceptable ambiguity with regard to parsing, or if it has some other fatal flaw that I'm not realizing. I was able to read each of these and clearly understand exactly what initializer would be generated, and it provides a smooth path from invisible synthesized initializer to big, thorny, fully-customized initializer, in the sense of that there are no behavior cliffs (where needing one extra little thing would suddenly necessitate writing a huge amount of new code). The default value part is the only thing that I felt slightly suspicious about, but I just need to think about it some more.

I suppose the feature description might be:

Currently in all function signatures, including initializers, all parameters must have an explicitly written type. The feature could be that, only in the parameter list of initializers, the type of a parameter can be left off if and only if the internal argument name matches a stored property of the type being initialized. External argument labels and default values are still permitted for this type of parameter. The value of such a parameter will be automatically assigned to the corresponding property before the body of the initializer is run, meaning that if the property is declared with let it cannot be explicitly assigned in the body of the initializer. You can always explicitly add the type to the initializer parameter as is mandatory today to return to normality and turn off this automatic assignment to the corresponding property. Lastly, if all of the parameters of an initializer are "auto-assigning" in this way and no stored properties of the type are left out then you do not need to write any initializer body (although if parsing-wise this is too difficult/undesirably messy it's not necessary because a trailing empty body is very easy to write and with this description of the feature that I've just given an empty body would simply trigger all of the auto-assignments and then do nothing else and then return).

3 Likes

Even with this feature we might still want to talk about an additional cherry-on-top syntax for the most common case, which is to generate a strictly memberwise initializer with no extra customization, such that this could be expressed without having to repeat every parameter name in order. On the other hand, maybe at least requiring the parameter names written in order when making a public memberwise initializer nicely handles an important point from the other side of the debate which is not wanting to make it too easy to accidentally introduce breaking changes to a package.

Possible "cherry-on-top" syntax:

init(...)

I'm still not crazy about the ... syntax. Another thought I had is that maybe it would be better that any initializer which makes use of auto-assigning parameters is required to be marked with something like auto, leaving the bare auto init to mean "the canonical memberwise initializer".

// Module A
public struct Circle {
    public var x: Double
    public var y: Double
    public var radius: Double

    public auto init { } // Generates the full memberwise initializer
    public auto init (centeredOnOriginWithRadius radius) {
        x = 0
        y = 0
    }
}

// Module B

import CircleModule

func circlesCanBeCreatedLikeThis () -> (Circle, Circle) {
    (
        Circle(x: 1, y: 2, radius: 3),
        Circle(centeredOnOriginWithRadius: 7)
    )
}

I'm now leaning towards the idea that this feature should not mess with the concept of eliding the initializer body, because I now feel like we have a better alternative which doesn't involve setting a precedent like that, and the fewer boats we can rock while still achieving the goal the better.

To be clear, the thing I'm saying seems to me like the better alternative to eliding the initializer body is:

  • Introduce the concept of an "auto-assigning" initializer parameter.
  • It is written with no type and must have the same internal argument label as one of the stored properties.
  • Crucially, an auto-assigning initializer parameter is understood to have the behavior that it assigns its value to its corresponding stored property before the initializer body is run, and therefore those properties are accesible on self within the body of the auto init.
  • (?) Maybe the initializer should be required to be marked with something like auto in order to opt in to being able to use auto-assigning parameters, which a) keeps this feature out of the hair of anyone who wants to keep it old school, b) adds perhaps a bit of educational clarity for people who aren't familiar with the feature and stumble upon it in someone else's code (at least gives them something clearer to search for), and c) leaves a natural syntax for generating the canonical memberwise initializer (auto init { }) that doesn't step on any variadic toes...

The result of all this is that all the newly proposed syntax of this feature appears in the initializer signature, and the body can still be required without be a burden anymore because it can be empty, which is ideal because it leaves us a natural place in which to put any custom initializer logic if we need to, but imposes the minimum amount of "boilerplate" possible (which I imagine could be argued means it's not boilerplate...)

internal types already have this. If you have an internal struct with no other initialisers, it already gets an implicit internal memberwise init:

struct SomeStruct {
  var x: Int
  var y: String
}

SomeStruct(x: 42, y: "test") // This initialiser is implicit.

The only time you actually need to write the initialiser out is when you are using or exposing it in a way that makes source- or binary-stability commitments; such as if you want to make that initialiser public, @usableFromInline, or @inlinable.

In Swift, those additional commitments are supposed to be opt-in and require some explicit code by design. IMO, @1-877-547-7272's version is the best version of this idea because it ensures those exposed API declarations are written out.

At the same time, adding this would increase the complexity of the language, which IMO is not justified given how well the tooling-based solution works. So I'd still be -1 on even making that best version part of the language.

This seems to be exactly the thing that OP is struggling with:

And so, yeah - I just don't think this is a thing we should make easier. I think it goes against many of the core tenants of Swift's design as far as library evolution is concerned.

It might seem like something you want, or which would be convenient for your particular use-case, but it would be a dangerous feature for the language.

4 Likes

I don't think it's a good idea to remove the parameter types from the initializer declaration, as it could lead to unintentional source-breaking and ABI-breaking changes if the type of a stored property changes.

I do think that being able to add argument labels, reorder parameters, and add default values would be useful and Swifty.

I don't think we need the versions with additional logic. The additional logic doesn't really look additional — there's no indication that there's anything else is happening. I don't think it's clear enough that the parameters are assigned to properties. Instead, I think that if one wants to add additional logic to a generated memberwise initializer, they should have to create a new initializer that calls a generated initializer.

I'm not sure what you mean here. Both my proposal and the original proposal involve implicitly generating the memberwise assignments.

The difference is that my proposal requires the programmer to explicitly change the initializer declaration in order to make a source-breaking or ABI-breaking change while the original proposal would allow source-breaking changes and ABI-breaking changes to happen implicitly when the type's stored properties are changed or reordered. The programmer can still change the declaration and break the ABI, but that's no different than what can happen with initializers today.

It seems the parser is already able to parse bodiless initializers.

It should be noted that this is only true if all the stored properties of the structure are internal or public. If a stored property is private or file-private, then the default initializer will also be private or file-private and you'll need to explicitly write out the initializer if you want to promote its access level.

With that being said, I still don't think an ultra-terse memberwise initializer syntax (e.g. auto init {}) would be able to justify its complexity cost.

1 Like

As a data point: one of the reasons I tend to avoid modules when I can is the need to manually write member-wise initialisers. I'm afraid that's not just two of us.

2 Likes

I don’t have time to actively participate in SE anymore but I still follow the announcements. I noticed that the Language Working Group has returned SE-0018 for consideration and pointed to this thread in the announcement. It did not point to my most recent thoughts on the topic so I am doing that here. If anyone is interested, please see this thread: Explicit Memberwise Initializers.

3 Likes