[Pitch] Explicit opt-out for memberwise initialization

Hi everyone,

I'd like to propose a way to exclude individual stored properties from a struct's synthesized memberwise initializer. I'll use @Init(.ignore) as a placeholder spelling.

Consider a session that exposes its retry count for reading, but updates it through a method:

struct Session {
    var userID: String
    private(set) var retryCount = 0

    mutating func recordRetry() {
        retryCount += 1
    }
}

For this type, I want new sessions to start with zero retries. However, the memberwise initializer allows this:

Session(userID: "user1", retryCount: 100)

That's consistent with the current rules: private(set) restricts mutation, not whether a property participates in memberwise initialization. SE-0502 also deliberately leaves setter visibility out of that decision.

I can get the API I want by writing an initializer:

init(userID: String) {
    self.userID = userID
}

That's easy here. With more caller-supplied properties, though, excluding one piece of internal state means repeating all the other parameters and assignments.

I'd like to be able to write:

struct Session {
    var userID: String

    @Init(.ignore)
    private(set) var retryCount = 0

    mutating func recordRetry() {
        retryCount += 1
    }
}

// Synthesized: init(userID: String)

I propose that an excluded property use its declared initial value during synthesized initialization. The attribute would affect only memberwise synthesis; it would not change access control or the rules for manually written initializers.

For this pitch, I’d limit support to ordinary stored instance var properties in structs with explicit initial values. Property wrappers and lazy properties would be outside that scope. Stored let properties with declaration-site initial values are already excluded from memberwise initialization.

Exclusion would be opt-in. A type that supports restoring a saved session, for example, might reasonably allow callers to supply an initial retry count.

SE-0502 considered an exclusion attribute, but left it for future exploration as part of broader memberwise-initializer customization. This pitch takes a narrower approach: allowing individual properties to opt out while retaining synthesis for the rest.

I’d like to discuss whether that smaller feature stands on its own, and hear about cases where it would help compared with writing an initializer or using a macro.

I’m not sure that your example with the let is completely accurate. I tried with your example type and got this error:

<source>:8:22: error: extra argument 'id' in call
6 | }
7 | 
8 | let x = TodoItem(id: UUID(), title: "hi")
  |                      `- error: extra argument 'id' in call

Why do you feel that the complexity of writing an initializer by hand is higher than the complexity of introducing this new language feature? Do you have any examples where the private(set) issue happens in practice? (Your example type has an area property but that really should be a computed property)

Thanks for checking this. You’re right: a let property with an initial value is already excluded from the memberwise initializer. I got that wrong.

I’ve revised the motivating example to better reflect what I intended to propose.

The inconvenience I was trying to describe is that excluding just one property from the initializer requires manually writing the initialization code that Swift would otherwise synthesize for all the remaining properties. This becomes more repetitive as the number of properties grows. I’d like to keep that synthesis while being able to explicitly leave individual properties out.

In general, I don't think continuing to patch on new features to the language to handle specific sub-parts of memberwise initializers is the right approach.

This comment doesn't address this proposal specifically but is meant to be a broader idea. I've given this a lot of thought and I've come to the conclusion that something like this is the right shape for dealing with memberwise initializers:

struct Whatever {
  let alreadyInitialized: UUID = UUID()
  var x: Int
  var y: String

  @Memberwise init(x: Int, y: String)
}

It's true that there's a little bit more boilerplate by having to explicitly declare the signature of the initializer, since it repeats the names of the properties, but I consider that to be an important feature, not a flaw, and it's why it's specifically an attribute on the initializer and not on the type. Doing this has a number of advantages:

  • You have total control over the visibility of the initializer. You want it public? You write public init. We don't have to invent a bespoke way of spelling the visibility.
  • You have a place to hang the doc comment for the initializer. If it's public, you're certainly writing one, right?
  • It forces you to choose the order of the arguments and keep them stable, ensuring that an innocent rearrangement of stored properties doesn't break your API.

What's cool is you can get most of the way there today by creating a Memberwise function body macro that just synthesizes self.arg = arg for each argument in the signature. If you leave out some properties, the compiler will give you the usual diagnostics about not initializing everything.

There are some subtle quirks, like this requires you to put default values in the argument list of the init instead of assigning them directly to the property, which results in a different ABI than memberwise initializers today, but that would require language changes to make those property-initialized defaults visible to the macro.

15 Likes

Thanks for explaining this :blush:

I was focused on the repetition involved in writing an initializer just to exclude one property. Your approach removes the assignments while keeping the initializer’s API explicit, and I can see the value in keeping argument order independent of stored-property order.

One question I still have is about properties added later. Suppose I add a stored property with a default value and intend to make it configurable through the initializer, but forget to add the parameter. The compiler would accept that, just as it would an intentional omission.

Would you consider an opt-in check useful here—requiring each eligible property to either appear in the initializer or be explicitly marked as omitted? I realize that would likely need a separate type-level macro to inspect the properties, rather than the body macro alone.

I like that approach. Generalising a bit:

public struct Whatever {
  var x: Int
  var y: Int
  var z: Int = 0

  init(x: Int, y: Int, z: Int)           // traditional memberwise initialiser
  init(x2 x: Int, y: Int, z: Int)        // with parameter(s) renamed
  init(y: Int, x: Int, z: Int)           // with parameters reordered
  init(z: Int, y: Int = 0, z: Int)       // with defaulted parameter
  init(z: Int, y: Int, z: Int = default) // with defaulted parameter
  init(z: Int, y: Int)                   // omitting some parameters
  public init(x: Int, y: Int, z: Int)    // with explicit visibility
  @available(unavailable)
  init(x: Int, y: Int, z: Int)           // disabling memberwise initializer
}

I think that would be a useful feature. How best to express this, not sure.

HEADER init(x: Int, y: Int, z: Int) TRAILER

e.g. whether

  • HEADER is @Memberwise, or
  • TRAILER is an absent body, or
  • TRAILER is auto or
  • TRAILER is { auto } or
  • ...
1 Like

Definitely think @Memberwise init is the way to go.

With something like

@Memberwise init

@Memberwise public init

or maybe @Memberwise init(auto) or something that would list all the fields automatically as the current synthesised one does, so if any new fields are added the init declaration doesn't have to be updated.

1 Like

Maybe … to indicate all fields: init(…)

1 Like