SE-0400: Init Accessors

Hello Swift community,

The review of SE-0400: Init Accessors begins now and runs through July 10th, 2023.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to me as the review manager via the forum messaging feature or email. When contacting the review manager directly, please put "SE-0400" in the subject line.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at

https://github.com/apple/swift-evolution/blob/main/process.md

Thank you,

Frederick Kellison-Linn
Review Manager

17 Likes

I'm reluctantly in favor of this, "reluctantly" because it definitely makes the language more complicated, but "in favor" because it makes property wrappers less special.


With this proposal, all of self is initialized if:

  • All stored properties are initialized on all paths, and
  • All computed properties with init accessors are initialized on all paths.

This doesn't line up with the degrees/radians example, in which only the single stored property or the single computed property is initialized; nor does it work with existing property wrappers that sometimes are initialized directly (assigning to _x) and sometimes indirectly (assigning to x). I think the existing condition of "all stored properties are initialized on all paths" remains the correct condition to check.

Relatedly, can I have two computed properties that initialize the same stored properties? Or a shared subset thereof? If so, the above check definitely wouldn't work, and we'll see some fun new (but correct) errors from a synthesized memberwise initializer.


[EDIT]

init accessors are only called from within a module, so they are not part of the module's ABI. In cases where a type's initializer is @inlinable, the body of an init accessor must also be inlinable.

How use-sensitive is this? If the init accessor is used from one @inlinable init, diagnostics may start appearing in the body of the init accessor that weren't there before, correct? What if the init accessor is used from one init, but it's another init that's inlinable?

[FURTHER EDIT] Fun test case, if you don't have it already: @inlinable init initializes a (computed), which initializes b (also computed), which initializes c (stored).

10 Likes

That portion needs to be read in context with the following paragraphs which further explain that “initializes” as used in the segment you quote includes both:

  • transitive initialization of stored properties via initializes clauses for init accessors and
  • implicit initialization of computed properties with init accessors once all their own initialized stored properties are initialized

But I think with those caveats it comes out equivalent to just focusing on stored properties so that may be the better way to frame things for simplicity’s sake?

Edit: nope, as Jordan notes below you can have init accessors with no initializes clause and those must be explicitly initialized.

Mentioned briefly here as supported, but disables the memberwise init:

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.

2 Likes

Ah, I see, this is specifically for things like the dictionary-backed properties where skipping them should still be considered an error. Having to propagate initializes clauses both up and down the dependency tree seems a bit odd to me, but I guess there's not actually a problem with it!

2 Likes

Can the initialValue be implied, just like newValue is with set accessor? Otherwise, I find it asymmetrical with current conventions:

struct Angle {
  var degrees: Double
  var radians: Double {
    init initializes(degrees) {
      degrees = initialValue * 180 / .pi // initialValue is implied, just like newValue is with `set`
    }

    get { degrees * .pi / 180 }
    set { degrees = newValue * 180 / .pi }
  }

...
}

Also, a nice simplification might be to have the ability to bundle the init and set accessor so for less boilerplate where init/set have the same logic:

struct Angle {
  var degrees: Double
  var radians: Double {
    get { degrees * .pi / 180 }
    set init initializes(degrees) { degrees = newValue * 180 / .pi }
  }
...
}
6 Likes

How do these initializers interact with Codable, especially compiler-generated Codable conformance?

Depending on the answer to this question, could this then provide a solution to the "For @Wrapper var foo: Int?, the compiler generates decode(Wrapper<Int>.self, forKey: .foo) instead of decodeIfPresent()" problem?

1 Like

I have depended on this behavior in the past.

I fully support the idea but have to agree with Jrose on this.

Since the init to the computed variable is so special, could we just put this part in brackets like below and leave the body of the computed variable untouched?

struct Minimal {
    var value({ value: Int in
            ...
        }, 
        initialise(a, b, c),
        access(x, y, z)): Int {
        0
    }
}

@Douglas_Gregor @Jumhyn

I've had to spend considerable effort working around it without requiring non-compiler-generated conformance on the enclosing types; there's a lot of JSON out there that uses "missing" as a space/bandwidth-saving synonym for explicit null. It's why IkigaJSON has NilValueDecodingStrategy, and why Fluent's handling of Codable has ugly workarounds involving superDecoder(forKey:).

+1

So for using this proposal already; it takes an existing concept and breaks it apart into composable pieces. That decomposition lets us more generally approach problems and does not special case property wrappers. The spelling settled on seems quite reasonable and feels natural with the surrounding components.

This definitely addresses the problem scope well and lets other systems build on top of a more generalized system.

I have read the review threads, the proposal and used the feature in the development of another feature (Observation).

I feel like this will open up some new designs, but those will be considerably cleaner than the alternatives. Primarily this seems to me like an absolute MUST for macros that interact with types and generate fields.

5 Likes

Taking off my review manager hat for a moment, I have a few thoughts/questions:

  • I agree with @DevAndArtist's comment from the pitch thread:

  • I still don't like the restriction that the ability to synthesize a memberwise init depends on the relative ordering of properties and their accesses clauses. Since accesses clauses can only refer to stored properties there's no chance of cycle, and IMO we should synthesize a memberwise init with the parameters matching source order but with properties initialized in dependency order (ties broken by source order).

  • To Jordan's point above:

    is there a reason that we must support this (init accessors which don't actually initialize anything)? It doesn't seem necessary for subsuming property wrappers, so I'm curious if we just ought to leave it out. It seems somewhat esoteric. But if there's a super compelling use case I'd love to hear it! (E.g., is this support necessary for Observable?)

ETA:

  • Does invoking the setter of a stored property in an init accessor (can you do this?) invoke the didSet observer? IIRC the current rule is that setters called inside init never call didSet, seems like we should extend that to init accessors.

I noted myself as weakly against this direction in the pitch, but I think one thing to consider with the new syntax that puts the initializes and accesses clauses outside the init clause, a future direction which allows combining the init and set accessors is IMO significantly more awkward (it's not clear that initializes really only applies to the init and not the set). I guess you could just learn this, but it's something to consider...

3 Likes

No, we can't invoke the didSet observer, because observers have access to all of self

4 Likes

Yeah, I'm thinking of cases where it would be (dynamically) valid to call didSet, similar to:

struct S {
    var x: Int {
        didSet {
            print("didSet")
        }
    }
    var y: Int

    init() {
        self.x = 0
        self.y = 0
        self.x = 0
        self.f()
    }

    mutating func f() {
        self.x = 0
    }
}

S() // only prints `didSet` once

I was imagining that you need to write @inlinable on the init accessor explicitly in order to use it from an @inlinable initializer, but I can see how the proposal is totally not clear about that!

The implied parameter is called newValue, which is deliberately consistent with set. The reason to use newValue instead of initialValue is because the word "new" still fits with initialization; you are indeed providing a new value to the field when you are initializing it. I also wanted to avoid adding another magic parameter name that people need to discover and then remember. I don't really see a strong motivation to introduce initialValue instead of newValue here.

init accessors don't interact with synthesized conformances at all. Those conformances still look specifically at the set of stored properties. It's plausible that the generated conformance could use the same strategy that member-wise initializers have in this proposal, but that would be a semantic change to the way property wrappers behave today. However, this feature might allow you to write Codable conformance synthesis through a macro that has the behavior you're after, where the conformance is based on the "original" stored properties as written (e.g. before any property wrapper or macro transformations happen).

I don't think init accessors are any more special than get and set accessors.

It's not necessary for @Observable, which backs every computed property by a stored property. The dictionary storage example in the proposal is a proxy for use cases that back all properties by some external storage mechanism, such as @Model from SwiftData or the Realm use-case mentioned at the end of the SE-0385 motivation:

Consider the Persisted property wrapper from the Realm package:

@propertyWrapper public struct Persisted<Value: _Persistable> { ... } 

class Dog: Object { 
  @Persisted var name: String 
  @Persisted var age: Int 
}

To support advanced schema customization, the property wrapper could store a string that provides a custom name for the underlying database column, specified in the attribute arguments, e.g. @Persisted(named: "CustomName"). However, storing this metadata in the property wrapper requires additional storage for each instance of the containing type, even though the metadata value is fixed for the declaration the property wrapper is attached to. In addition to higher memory overload, the metadata values are evaluated eagerly, and for each instantiation of the containing type, rendering property-wrapper instance metadata too expensive for this use case.

[...]

Combined with attached macros, the @Persisted property wrapper in Realm can evolve into a macro attached to persistent types, combined with custom metadata attributes that provide schema customization for specific declarations

I believe these use-cases really do want diagnostics in the case where you forget to assign to one of the computed fields in an initializer.

Ah, we totally could make this work using the same strategy in definite initialization that chooses between an init accessor call or a set accessor call, but this proposal does not include this change. However, if an assignment to a computed property in an initializer is re-written to a set accessor call and that set accessor assigns to a stored property, the observer for that stored property will indeed be called (this is not new behavior in this proposal). But inside of an init accessor, property observers are never called.

1 Like

You might be right. It really depends from which angle we see it.

My arguments would be:

  1. init is different from getter and setter because it participates the type initialisation process.
  2. this syntax will leave getter and setter syntax untouched, which mean even we have init method, we can still return value without having to use get {}

But I agree it's true that this syntax is kind of alien to current Swift syntax. Same as the proposed init method :slight_smile:

Again, I fully support the idea. These are just syntax considerations. Thanks.

2 Likes

i think that this change is needed, altough it makes the language another bit more complicated, a direction that i know is needed by the necessities of us all, but i'm bit scaried of the risk of becoming "a gargantuan language" just like c++ with all this keyword and syntaxes

I just have a curiosity, this change would also enable apple engineers to resolve problems like the "core data requires that the not optional values should have a default value." when using SwiftData+CloudKit on the iOS17 Beta?

Assuming I'm understanding this proposal correctly, this is trying to solve the problem that you can't really do anything in an initializer without falling afoul of definite initialization rules, because the compiler doesn't "peer into" the implementation of what you call. Technically, what I would say is happening is that self is not fully initialized at the point of the thing you're trying to call, and the proposal is trying to specify a way to hand around a partially constructed object by explicitly specifying which parts of self it is allowed to touch and which parts it initializes.

The reason I describe this so generally is that the proposal seems to have a motivating example of property wrappers/macros, but I don't actually think property wrappers are the most common gap where something like this is needed. Instead, it looks something like this:

struct Foo {
	enum Bar {
		case a(A)
		case b(B)
	}
	let bar: Bar
	let common: SomethingInCommon

	init(a: A) {
		bar = .a(a)
		commonInit()
	}

	init(b: B) {
		bar = .b(b)
		commonInit()
	}

	func commonInit() {
		common = doSomeCommonInitialization()
	}
}

This doesn't work today, of course: you can't can't factor out the common parts of initializers because you can't call anything on self yet, and the other functions aren't allowed to initialize read-only members anyways. This new proposal does actually give a spelling to this pattern, but it reads pretty poorly, probably something like this:

var _commonInit: Void {
	init initializes(common) {
		common = doSomeCommonInitialization()
	}

	get { fatalError() }
	set { fatalError() }
}

Obviously, this is because we're abusing a feature built for computed properties to do more general initialization. I think what this really shows is that the need here goes beyond just properties. To that end, I would like to see consideration of the concept of "this block of code accesses these fields and initializes some other fields, and is morally kind of operating as if it was part of an initializer" rather than "this property wrapper accesses these fields and initializes some other fields, and is morally kind of operating as if it was part of an initializer". It may be that we decide that we want to start with property wrappers for now and punt the more general version to future directions, but I do think it is important that even if we do that the syntax we pick now has a natural extension to this more general case should we choose to implement it at some point.

13 Likes

Yes. That issues comes from the Observation macro. If you use self.property = … inside an init from an observable type, that means that you're calling the computed property and not really initializing the stored property. That's why this feature would resolve the problem as the Observable macro would start synthesizing the init accessors to properly reroute such assignments via the computed properties to their baking storage.

3 Likes

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