If we were to generalize initializes
and accesses
effects to other functions, then the lack of initializes
means self
is already fully initialized, and the lack of accesses
means the function accesses all of self
. But init
accessors and initializers are special, because specific fields within self
become available as they're initialized, so the lack of accesses
does not mean they have access to all of self
.
Would this feature, then, complicate or prevent the previously-requested public memberwise init
shorthand?
My preference is to wait for the need to arise before adding a complicated behavior to this proposal. Because this pattern won't compile under this proposal, it's a restriction that we can lift later, and of course the workaround in the meantime is to write an initializer yourself with the properties in whatever order you want.
My same comment above applies here, and I'm definitely concerned about this
If the programmer really does want a combinatorial explosion of initializer overloads, IMO it's better for the programmer to endure writing them explicitly so that they have a visualization of the impact their overloading pattern will have, e.g. on overload resolution performance
As I said above, Iām satisfied that the proposed behavior is reasonable.
My point is specifically directed at the text: I think it ought not to use the argument that the userās intention is recognizably āmost likelyā to have multiple memberwise initializers in order to justify not giving them any memberwise initializers at all!
Iām sold on introducing this generalization. Making property wrappersā memberwise init magic less magical is a worthy goal. Two specific suggestions/comments:
- I donāt understand why we introduce two new ad-hoc grammar productions, init-effect and access-effect, instead of using modifiers or attributes on the accessor:
var radians: Double {
@initializes(degrees) init(initialValue) {
degrees = initialValue * 180 / .pi
}
get { degrees * .pi / 180 }
set { degrees = newValue * 180 / .pi }
}
This would mean that general attribute features (e.g. hasAttribute
, #if
in attribute lists, attribute code completion) will also be usable with this feature. āAlternatives consideredā discusses attributes on the property, but not on the accessor, and I donāt think any of the listed rationales apply to attributes on the accessor.
- I wonder if it would now make sense to allow initializer clauses on computed properties with an
init
accessor:
var radians: Double = 0 {
init(initialValue) initializes(degrees) {
degrees = initialValue * 180 / .pi
}
get { degrees * .pi / 180 }
set { degrees = newValue * 180 / .pi }
}
Because @anything
@could
@be
@an
@attribute
.
I avoid making claims about what Swift āneedsā or āshouldā do, but in this case Iām willing to say that Swift is long overdue for a guideline that separates @attributes
from other syntax.
If we initialize in dependency order, any side effects from initialization will come in an order that is hard to reason about. OTOH, it's also hard to reason about why the memberwise initializer isn't getting defined in the current proposal. I generally feel that compile-time surprises are better than runtime surprises, so I lean more toward what's in the proposal now. However, here's an idea: what if in the cases where the proposal says we suppress the memberwise initializer, we instead make it unavailable, and the compiler can put in a message describing why it's unavailable.
The memberwise initializer is already source-order dependent.
This is my favorite part of the feature, because it's what let's us completely abstract away the manner in which a type's stored properties are actually stored, without changing the way in which they are initializer or accessed by clients. The dictionary-backed properties example is perhaps too abstract, but I'd also want to use this for (e.g.) the "bitfield" macro I've been itching to write (where storage goes into a bag of bits) and a copy-on-write helper macro (where storage goes into a synthesized class). So while you're right that we don't need this if our only goal were to give a feature on which property wrappers could be built, this part is what brings the valuable expressive power.
Remember that didInit
would not have access to all of self
, so it would be pretty limited. Personally, I don't think we should add more observers like willSet
or didSet
to the language. The combination of get
/set
and init
proposed here should be enough to build a macro to provide didInit
functionality.
That's essentially one goal, yes. Not that property wrappers would be removed per se (they're in widespread use), but that they become something that is a layer of syntactic sugar over something you could do with a macro, with no special rules.
Folks seemed to have a fairly strong negative reaction to the use of attributes/modifiers initially, so we proposed the effects. I don't know that either of us authors feels strongly about the syntax here.
I think it makes sense. The initializer would be used as a default argument for the memberwise initializer, and used for initialization in each designed initializer.
Doug
An explainable failure is definitely better, and I take your general point re: runtime vs. compile-time, but Iām having a little trouble visualizing what the worst case looks like in terms of out-of-order side effects. Since the entries in the accesses
clause must be stored properties, it seems like the potential for surprise side-effects is quite limited.
This looks very good overall, and I have a question about expressing an "initialize, but don't allow set afterwards" properties using this feature.
I could not quite confirm from the proposal text but is my understanding that:
struct S {
var x1: Int
var x2: Int
var computed: Int {
init(newValue) initializes(x1, x2) { ... }
}
init() {
self.computed = 1 // initializes 'computed', 'x1', and 'x2'; 'self' is now fully initialized
self.computed = 2 // error, already initialized, no set{} accessor
}
}
is that the case or would one be allowed to set the value?
Context: I'm looking to compiler guarantee no-one changes a value after they've initialized it in a rather tricky property wrapper dance that we do in the cluster library:
distributed actor DistributedEcho {
@ActorID.Metadata(\.receptionID)
var receptionID: String
init(greeting: String, actorSystem: ActorSystem) async {
self.actorSystem = actorSystem
self.receptionID = "*"
}
where the property wrapper is:
extension ClusterSystem.ActorID {
@propertyWrapper
public struct Metadata<Value: Sendable & Codable> {
let keyType: Any.Type
let id: String
public init(_ keyPath: KeyPath<ActorMetadataKeys, ActorMetadataKey<Value>>) {
let key = ActorMetadataKeys.__instance[keyPath: keyPath]
self.id = key.id
self.keyType = type(of: key)
}
public var wrappedValue: Value {
get { fatalError("called wrappedValue getter") }
set { fatalError("called wrappedValue setter") }
}
public var projectedValue: Value {
get { fatalError("called projectedValue getter") }
set { fatalError("called projectedValue setter") }
}
public static subscript<EnclosingSelf: DistributedActor, FinalValue>(
_enclosingInstance myself: EnclosingSelf,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, FinalValue>,
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Self>
) -> Value where EnclosingSelf.ActorSystem == ClusterSystem, EnclosingSelf.ID == ClusterSystem.ActorID {
would I be able to prevent people setting a different value after they initialized it? Probably no since I'm using the enclosing instance dance... but figured I'll ask.
Yes, Holly and I discussed this before and agreed that should work but it hasn't been implemented for user-defined initializers yet.
Edit: I should add that for user-defined initializers init accessors are used only when setter is present at the moment, everything else falls back to regular assignment. Memberwise initializers work even when there is no setter because it's possible to emit init accessor directly and we don't allow intersecting initializations.
Not sure if the ship on this proposal has sailed or not yet, I just have a naming suggestion (which most likely has been discussed before).
For the same reason the name of initializer on a type is called init
, I think it makes it closer to the same syntax if we use inits
instead of initializes
.
struct S {
var x1: Int
var x2: Int
var computed: Int {
init(newValue) inits(x1, x2) { ... }
}
init() {
self.computed = 1 // initializes 'computed', 'x1', and 'x2'; 'self' is now fully initialized
}
}
it would also avoid British vs American style of english words in the language.
In response to the feedback from this review, the authors have made a number of changes to this proposal.
- Rather than the initialized and accessed properties being specified as part of the effects clause, they are now specified as part of the (new)
@storageRestrictions(initializes:accesses:)
attribute attached to theinit
accessor. - The body of the memberwise init will be synthesized such that properties are initialized in the order necessary to satisfy the
accesses
lists of anyinit
accessors. - Several clarifications based on questions from the review thread.
Please see the PR linked above for a full accounting of all the edits as well as specifics. The main proposal document is up-to-date.
In order to give the community time to consider and discuss these changes, the review of SE-0400 will be extended until July 10th.
I like the move to use an attribute because that makes the syntax less special. I wonder if we can make it even less special by using keypath syntax (use initializes: \.x
instead of initializes: x
).
I find the name storageRestrictions
cumbersome and hard to remember. I have nothing against long names if it adds clarity, but to me, it doesnāt. Maybe a shorter name or using two attributes @initializes
and @accesses
could be used.
To get the \.x
syntax to work, we'd need to have Self
as the implicit root for the key paths passed to initializes
and accesses
. And we'd not have a way of working with global or local properties.
Happy to consider a new name, but I'd strongly prefer not to have multiple attributes for this.
Doug
Happy to see the attribute over the effects syntax, but is there any reason it can't simply be @initializes(_:accessing:)
?
The various permutations seems to make sense and read well:
Initializing only:
@initializes(x, y)
var someVar: Float { ... }
Initializing and accessing:
@initializes(z, accessing: x, y)
var someVar: Float { ... }
Accessing only (if that's even possible):
@initializes(accessing: x, y)
var someVar: Float { ... }
I am confused by the section Init accessors on computed properties where it says āFor example, given the following:ā and then shows a code block and has nothing further, starting a new section immediately after the code. Given that code, then what? I expected it was going to specify what was implied for the memberwise initializer, but there was nothing there.
I understand why privileging initialization makes sense in the context of init accessors, but it makes less sense if you think of this as eventually becoming a more general feature. For example, suppose you have a helper method that you want to make callable from either init
or an ordinary method; such a method can never have an initializes:
clause, only an accesses:
clause. So in your suggestion, we'd either need a new attribute or to spell it @initializes(accesses:)
, which is just strange. Similar ideas apply to methods that could be called from deinit
.
On the other hand we could have this:
@accesses(var1, var2, initializing: var3)
and just consider initialization as a special kind of access.
You are correct; update here: [SE-0400] Finish the exampel for "init accessors on computed properties" by DougGregor Ā· Pull Request #2098 Ā· apple/swift-evolution Ā· GitHub
Doug
I suspect this feature could now be implemented as a macro, perhaps replacing the existing implementation thatās in the compiler. (Is it currently possible to have implied macros? I donāt know.) I doubt this proposal would have a detrimental effect on that, though I would like to hear the authors weigh in on that question.