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

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.

1 Like

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.

2 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