SE-0400: Init Accessors

The last development snapshot available for download is from June 7. it would be great to have most recent one with this experimental feature available.

+1 from me.

A memberwise initializer will not be synthesized if a stored property that is an accesses effect of a computed property is ordered after that computed property in the source code:

This limitations seems artificial. Compiler should be able to compute topological sorting of the initializations. Or am I missing something? But that's a really minor thing. If it comes with a noticeable performance hit, I'd rather manually reorder property declarations once, than have increased compilation times on every build.

Do I understand correctly that initializes and accesses can list only stored properties?

That is a bit unfortunate. The feature itself attempts to provide abstraction over initializing stored vs computed properties, but in this aspect abstraction is leaking - when initializing property inside init accessor one needs to know if it is a stored or computed property. Does macro API allow to query that and obtain a list of properties in the effects?

1 Like

Great, this is exactly the extension I’d expect of the current “never call observers from assignments that are lexically within an init” rule.

I am not ignoring you. I incorporated your capture-list-style syntax suggestion, along with the other suggestions at the end of the pitch thread, into the Alternatives Considered section, and I already justified (twice) in the pitch thread why we cannot simply infer initializes and accesses.

8 Likes

When the init accessor initialize all of self, is initializes(self) a valid spelling?

Because initializes and accesses are modeled as effects here, it seems plausible that a follow-up proposal could allow using those effects on instance methods. Those methods would then have the same semantics as init accessors - you cannot access all of self, you can only access the stored properties in the accesses list, and you must initialize the properties in the initializes list on all paths. There would be some other restrictions on where you're actually able to call those methods, and which other methods are allowed to be called from inside the implementation, but I don't believe anything in this proposal rules out this direction.

3 Likes

The problem with C++ isn't really the number of features, it's the number of features that both completely replace existing features while either having their design severely compromised for compatibility with prexisting features or being incompatible with those features in unpleasant and subtle ways. Often both.

All this plus WG21's bizarre fetish for making things that ideally would have some language support into library only features (std::optional, std::variant, std::forward & move, etc.).

Swift is a lot better about this because most of the new features (that aren't just sugar) don't really replace old features, tend not to have baffling interactions with existing features, and mostly only need to be known and understood by the person actually using the feature because Swift likes progressive disclosure.

Back on topic: It makes property wrappers less magic, but it's a bit verbose. I don't see a way to make it more concise without making it an unreadable mess, so I'm tentatively for this proposal. +0.5

3 Likes

Would it be feasible to add a didInit listener to a stored property, the same way one can add willSet or didSet without having to make it computed?

Maybe the initialized properties could be an effect similar to mutating/nonmutating in setters/getters. These modifiers declare how implicit self is passed. For instance, in a struct property's setter, self is passed as inout self by default. Similarly, if we generalize init to the initializing ownership modifier (similar to borrowing and consuming), self._initializedProperty would be passed as initializing _initializedProperty.

struct MyStruct {
  var _a: Int
  var a: Int { 
    nonmutating get { // Implies: `[borrowing self: Self] in`
      _a 
    } 
    mutating set { // Implies: `[mutating self: Self] in`
      _a = newValue
    }
    initializes(_a) init { // Implies:
       // `[initializing self._a: Int] in`
      _a = newValue
    }
  }
}

As for the awkwardness of the future direction combining the set and init implementations, we could just separate the accessors with a comma:

// ...
mutating set, initializes(_a) init { 
  _a = newValue
}
1 Like

@Jumhyn has made the same argument. Setting aside whether or not it's feasible, I think what's currently in the proposal leads to the most understandable behavior, because the order of the parameter list should match the order of initialization in the member-wise initializer (which I do not think we should change), which means that a change in dependencies could break all call-sites of your initializer. Topological sorting also just doesn't seem worth it the complexity to me. Do either of you have any use cases that would greatly benefit from this behavior?

That's correct. accesses fundamentally cannot list computed properties, because get accessors have access to self (unless we had this generalization of these effects). Perhaps initializes could be generalized to include computed properties with init accessors in the future.

initializes and accesses are part of the syntax tree, so a macro can gather all of the listed property names.

1 Like

Seems good.

Unfortunate to have to add more syntax and complexity to Swift, but the proposal does seem like a solution to the problems it aims to solve.

The one thing that stood out to me was:

A memberwise initializer will not be synthesized if a stored property that is an accesses effect of a computed property is ordered after that computed property in the source code

I do not generally expect the order of code in a file to be significant and since the change is the invisible generation of code, or not, this seems likely to trip programmers up (especially those learning the language).

Perhaps I’m just forgetting other places order in a file matters in Swift, but this seems like a particularly awkward one.

Any chance there are other solutions that might remove this subtlety?

1 Like

Maybe I'm not communicating my thoughts clearly. I am not concerned with the name initialValue vs newValue. I'm more asking about why the parameter is hidden or implied with the set accessor but it needs to be specified with the init accessor. To me, if it's hidden/implied with the set accessor, I would expect the same with the init accessor.

I also wanted to avoid adding another magic parameter name that people need to discover and then remember.

If we don't like the magic parameter, should the current set accessor syntax then be somehow deprecated?

2 Likes

So, I have a different view on this: IMO adding an accesses(y) clause to the init accessor on x expresses a clear intention that y be initialized before x, in the same way that the proposal claims that initializes(y) expresses a clear intention that y be initialized via x.

I also don't feel like the interface exposed via the memberwise init ought to depend on the implementation detail of which properties happen to be accessed in the initializer, it should be based on what order of properties makes sense at the initializer use site. That said...

I don't have a good example off the top of my head of a situation where:

  • some parameter order obviously makes more sense at the use site, but
  • dependency via accesses violates this order

I don't think this needs to be resolved as part of this proposal since we could always add the capability later if we needed to, I just worry it will push users towards potentially undesirable property orderings in favor of keeping the memberwise init around.

7 Likes

You can spell it explicitly, using set(customName) { }. But is it worth breaking all the code out there that uses the implicit newValue?

1 Like

I believe you're misunderstanding the proposal, which seems quite clear on this point:

The identifier in an init-accessor-parameter , if provided, is the name of the parameter that contains the initial value. If not provided, a parameter with the name newValue is automatically created.

The examples in the proposal happen to include an explicit parameter, but that's not required.

3 Likes

Ah! Thank you I had missed that. That's perfect. I'm sorry for the confusion!

Is it accurate to say that without any initializes() or accesses() modifiers, a computed property implicitly accesses(self)?

I think my feelings about this proposal mostly fall in the “if we must…” camp. It reminds me of member initializer lists in C++, which exist so that all members can be initialized before the constructor itself begins executing. The model being proposed here is more flexible, and therefore more complicated. But I’m generally in favor of generalizing ad-hoc features like those that support property wrappers—or property wrappers themselves. I believe the plan is for macros to fully supplant property wrappers in time.

I would echo this concern, as well as @haikuty's. In general, code order affecting behavior can be annoying, but here the reason (IMO) for increased concern is that the effect is on synthesized—hence, invisible—code, which makes it all the more difficult to reason about.

I also agree with @Jumhyn that all the semantic information is there in source regarding user intention, as an effect on y spelled accesses(x) is explicit that y is a prerequisite of x. That the compiler understands but errors rather than complying seems like a missed opportunity.

We have somewhat of a precedent here (ha) in the design of precedence groups.


Relatedly, regarding the following:

Use cases for init accessors that provide a projection of a stored property as various units through several computed properties don't have a single preferred unit from which to initialize. Most likely, these use cases want a different member-wise initializer for each unit that you can initialize from. If a type contains several computed properties with init accessors that initialize the same stored property, a member-wise initializer will not be synthesized.

I actually don't object to the proposed behavior per se (i.e., not synthesizing any memberwise initializer at all when there are multiple computed properties that initialize the same stored property).

However, I don't think it's the conclusion that follows most directly from the rationale offered here. If, "most likely," the user's intention is to have multiple memberwise initializers, then it would seem to me that we ought to consider whether the compiler should recognize that intentionality and synthesize all of them instead of none of them:

struct Temperature {
  var _absolute: Double
  var celsius: Double {
    init initializes(_absolute) { ... }
    get { ... }
    set { ... }
  }
  var fahrenheit: Double {
    init initializes(_absolute) { ... }
    get { ... }
    set { ... }
  }
  // Makes sense to synthesize...
  init(celsius: Double) { ... }
  init(fahrenheit: Double) { ... }
}

Seems like “all of them” would explode exponentially if you have multiple stored properties with more than one computed property that initializes it.

2 Likes

Indeed; but then it'd also be implausible that a combinatorial explosion is "most likely" the user's intention.