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

The Problem

Let's say I'm writing a module that will be imported somewhere else, and a struct that I define in my module needs to be initializable outside of my module. I am forced to write out the memberwise initializer for my struct by hand, because only then can I mark it public - the one that is synthesized for me will have internal access if my struct is public, meaning that the initializer won't be accessible from outside my module. This memberwise initializer that I have to write out and maintain is arbitrarily large because my struct can contain an arbitrary number of properties. I think this is referred to as a behavior cliff or something like that - a theoretically incremental increase in my needs causes a sudden large increase in my boilerplate, introducing room for errors aside from being a drag.

My Proposed Solution (or I'd love to hear why it's a terrible idea)

What if I could resolve this situation by writing:

public struct MyStruct {
    public var foo: String
    public var bar: Bool?
    public var baz: Array<Int>
    
    public memberwise init
}

Introducing the memberwise keyword, only to be applied before an init declaration with no parameters and no body, would allow us to customize various aspects of the canonical synthesized initializer for a given struct without having to write out the obvious part. This would allow us to do things like:

struct MyStruct <T> {
    public var foo: String
    public var bar: Bool?
    public var baz: Array<T>
    
    public memberwise init where T: Codable
        // For whatever reason we want to allow outsiders to create the struct only if the generic parameter is Codable

    internal memberwise init
}
struct MyStruct <T> {
    private var foo: String
    private var bar: Bool?
    private var baz: Array<T>
    
    public memberwise init
        // Here we get to allow outsiders to initialize the members but not read them thereafter.
}

Thoughts, reactions?

20 Likes

This is essentially SE-0018, deferred from Swift 3 by the core team over six years ago:

8 Likes

Not sure whether it’s feasible or not (or has already been suggested before), but the alternative [syntax] that stands out to me is what I’ve seen (and written) in documentation for really long initializers — init(...) (including the ...). Also avoids needing to add yet another (fairly generic) keyword for such a narrow scope. :smile:

[Edit: I see that the ... syntax was indeed part of the original SE-0017 proposal along with the keyword, though IMHO the syntax doesn’t seem confusing and still seems like the natural ‘truncation’ of a long initializer (implying compiler synthesis. But I’ll leave my feedback at that.]

2 Likes

I had a look at SE-0018 and some of the feedback, and it seems like there was a lot positive feedback about solving something in this space but a lot of negative feedback about two things in particular:

  1. Giving a new meaning to which people worried could conflict with variadics

  2. The feature being overall too complicated/difficult to explain, with many exceptions and special rules

It seems to me like my proposal is a simplified version of SE-0018 and that the simplification solves both issues. 1) I'm not suggesting using ... (which is something I would have given negative feedback about too) and 2) as far as I could tell from my somewhat cursory glance, SE-0018 allowed customizing the memberwise initializer by adding additional parameters or changing their order or something like that, but what I'm proposing here completely removes all of that. Explaining the feature is extremely simple, especially if you're already familiar with synthesized initializers: if you add no initializer to a struct, the compiler implicitly adds internal memberwise init for you (or private or fileprivate if the struct is private or fileprivate), but you now have the option to add that declaration explicitly and therefore change the access control (and even add a where clause if you want, although if that adds some unforeseen-by-me complexity then we could drop it).

Does my assessment seem accurate to you or am I missing something?

4 Likes

I realize now that perhaps a better keyword would be synthesized:

public struct MyStruct {
    public var foo: String
    public var bar: Bool?
    public var baz: Array<Int>
    
    public synthesized init
}

I've updated the post.

2 Likes

I think memberwise is much better. It describes how the initializer is synthesized, not just that it is.

15 Likes

You’re argument has convinced me, I think you’re right, I removed the edits

2 Likes

This is something that would be hugely beneficial to actual developers of apps and libraries for apps. Having to reimplement the init just to make it public for every public struct in a library is very annoying.

For the keyword, would anything else benefit from not having to redeclare the synthesised implementation in the future? Eg Codeable or something. In that case synthesized would be better than memberwise, and as it is now there is nothing to say how the init is implemented anywhere, developers just have to know that the synthesised one is a memberwise init.

1 Like

Wow, I wasn’t expecting to have my mind changed back and forth so many times… now I’m leaning towards synthesized again… because, as you point out, it is already expected that developers know the nature of the various synthesized implementations that the compiler offers (initializers, Codable, Hashable, etc.)

@memberwise Differentiable was the proposed spelling for automatic differentiation, and it is conceivable if we wanted to label the synthesis of conformance explicitly that we’d opt for @memberwise Codable, etc.

For alignment with the core team’s decision as it now stands, which is that there is nothing to mark a synthesized implementation, a synthesized memberwise initializer would be spelled init with no attribute whatsoever.

1 Like

I’m worried that this might not be very searchable. We may name this feature something like bare init, but new users may not think to search that. With a keyword however, users may directly search the feature and could even be directed straight to the Swift website. Is there any precedent for this in other languages so that users might expect this?

3 Likes

This is one of those things that's always felt like a missing feature to me. One suggestion I have is that we could gain some flexibility by providing a memberwise parameter placeholder rather than a placeholder for the entire initializer:

init(#members, otherParams: String...) {
    // Setting members is synthesized here
    // ...
}

This approach lets you write init(#members) {} if you just want a regular memberwise init, but you can also add extra parameters and logic to a memberwise init.

I'm not sure what the most appropriate spelling for the placeholder would be, maybe just undecorated members? Either way, I think the extra flexibility would come in handy.

3 Likes

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