[Pitch] `init` accessors

I agree that mentioning other properties in what looks like an argument list seems a bit strange. As an alternative to having these after the init, would using an attribute be an option?

struct S {
  var readMe: String

  var _x: Int

  var x: Int {
    @initializes(_x)
    @accesses(readMe)
    init(newValue) {
      print(readMe)
      _x = newValue
    }

    get { _x }
    set { _x = newValue }
  }
}
7 Likes

It benefits the programmer writing the init accessor. You state your assumptions about what the init accessor initializes and depends on, and the compiler will provide error messages if you accidentally initialize or access something you didn't mean to, or forgot to initialize something that you meant to initialize. If you don't get diagnostics in the body of an init accessor and you make a mistake, you'll probably get misleading diagnostics somewhere else (probably in the body of the type's initializer) because the compiler won't have any way to infer that you just forgot to initialize something in the accessor.

Me! I wrote this code without thinking any thing of it (I'm not a numerics expert!). I don't think it's that ridiculous, and even if it is, the example was meant to illustrate the general pattern of computing values from stored properties via computed properties in ways that don't fit the property wrapper model, which I personally have used all over the place in my Swift code, and I suspect others have as well.

EDIT: I have been informed by a numerics expert that degrees are bad! I stand by my point though; I still think a stored value in some representation that is used to compute the same conceptual value in another representation is a very common pattern that doesn't fit into the property wrapper model, it's reasonable to support virtual initialization from any representation, and I can change the proposal example to something else if Angle is actually that distracting :slight_smile:

This proposal doesn't add any expressivity that you cannot already write in some form in your code. Property wrapper definite initialization doesn't add any expressivity either -- you can always manually initialize the backing property wrapper yourself, but people find that annoying / confusing because the backing property wrapper storage is an implementation detail that often isn't otherwise mentioned at all in code using property wrappers. The proposal text also outlines a usability issue with attached macros due to the lack of custom definite initialization behavior:

Furthermore, property-wrapper-like macros cannot achieve the same initializer usability, because any backing storage variables added must be initialized directly instead of supporting initialization through computed properties. For example, the proposed @Observable macro applies a property-wrapper-like transform that turns stored properties into computed properties backed by the observation APIs, but it provides no way to write an initializer using the original property names like the programmer expects:

@Observable
struct Proposal {
  var title: String
  var text: String

  init(title: String, text: String) {
    self.title = title // error: 'self' used before all stored properties are initialized
    self.text = text // error: 'self' used before all stored properties are initialized
  } // error: Return from initializer without initializing all stored properties
}
3 Likes

The idea sounds wonderful to me, because it's solving a real problem that I myself have encountered, as well as making the currently in-review proposal Observation a lot more viable.

The syntax (in my opinion) is overall good, but I'd rather refer to the properties in the initializes and accesses sections in the form of a key path expression, because simply mentioning them by name in a place that looks like a parameter list feels like it means to read their value right then and there, which is not what it's doing. A key path expression, on the other hand, clearly means a reference to a property without accessing it.

2 Likes

So it's like an inline unit test. While that may be helpful, it seems like a very weird thing to introduce a new language feature for, in all other situations we have to keep track of what we want to access etc ourselves. Is this really the actual reason, or just incidental?

If on the other hand the purpose is that we want to allow partial delegation of inits, then that makes sense and should be broadened to functions. Though then we wouldn't need to specify anything manually, the compiler knows what's going on.

If the pitch is that sometimes you need to set backing store differently during init than during mutation - especially to satisfy initialisation rules, then that seems like a useful feature but I really can't see why we'd want to tack on this inline unit test. It seems like an orthogonal feature.

And again, why aren't they keypaths?

When it comes to having an init accessor at all, I think it's very hard to evaluate its usefulness if no actual examples are provided where it makes a difference. It seems like it's just set by another name, with built in unit testing.

About the Angle type, that was a joke. I've often made similar constructions myself, it just seems very strange to an ex mathematician to use degrees as the backing storage (unless it's an Int perhaps). It's a bit like storing a number as a String.

2 Likes

I like the future direction you've outlined with the sum() example a lot. I hope we can find a solution that keeps this door open for us, because it would fulfill a real need for me.

Personally, I would prefer the already mentioned attributes directions, because I already see them as a way to give the compiler more contextual information and with examples like escaping I also don't mind, that they are sometimes even required.

@initializes(_value)
init(initialValue) { ... }

For a few moments I wondered if square brackets could also be an option, but I feel the missing in and that this feature is not about objects memory lifecycle makes it a bad fit.

init(initialValue) { [initializes _value] ... }
1 Like

The proposal seems helpful and interesting. However, I am struggling with the syntax as many others are. I do think attributes (@initializes) would help. I also wonder if key paths would be better suited.

Also - does the init accessor need to take a parameter like a function? Or can that be implied like newValue is implied in set accessor?

1 Like

This example has me thoroughly confused:

struct S {
  var x1: Int
  var x2: Int
  var x3: Int
  var computed: Int {
    init(newValue, initializes: x1, x2) { ... }
  }

  init() {
    self.x1 = 1 // initializes 'x1'; neither 'x2' or 'computed' is initialized
    self.x2 = 1 // initializes 'x2' and 'computed'
    self.x3 = 1 // initializes 'x3'; 'self' is now fully initialized
  }
}

Why does self.x2 = 1 cause computed to be initialised? Surely it's the other way around, since computed is annotated with initializes: x1, x2 it would initialise x2, but what is that causes the reverse of that? There is no reason to assume these relationships are reciprocal. Isn't that what accesses is for?

2 Likes

I'm still digesting this, but the basic idea makes sense to me. A couple questions, thoughts:

  • I think I'd like to see accesses justified a little more thoroughly. It doesn't seem necessary for the 'subsume property wrappers' goal, and in the single more concrete example with dictionary, the accessed property starts off with a default value, so it seems like we could get away without accesses and just say that init accessors can only access properties which have default values. Would this feature feel truly incomplete without accesses?

  • Is it allowed for multiple different properties to initialize an underlying stored property? E.g.,

    struct Angle {
      var degrees: Double
      var radians: Double {
        init(newValue, initializes: degrees) {
          degrees = newValue * 180 / .pi
        }
    
        get { degrees * .pi / 180 }
        set { degrees = newValue * 180 / .pi }
      }
    
      var revolutions: Double {
        init(newValue, initializes: degrees) {
          degrees = revolutions * 360
        }
    
        get { degrees / 360 }
        set { degrees = revolutions * 360 }
      }
    
      init(degrees: Double) {
        self.degrees = degrees // sets 'self.degrees' directly
      }
    
      init(radians: Double) {
        self.radians = radians // calls init accessor with 'radians'
      }
    
      init(revolutions: Double) {
        self.revolutions = revolutions
      }
    }
    
  • If the answer to the above question is 'no', that is, an entry in an initializes: list is unique for a given property and type, might there be an alternate design here where the property declares that it is initialized by a given other property? E.g.

    struct S {
      var y: Int {
        get { x }
        set { y }
      }
      var x: Int {
        init(y) { // "when y is initialized, designate to this property"
          x = y
        }
      }
    
       // memberwise init
      init(y: Int) { // use y in memberwise init since it appears in init accessor
         self.y = y // calls x.init(y)
      } 
    }
    

    ETA: I don't really think I prefer this, and I see why the init accessor fits more naturally under the computed property rather than the transitively-initialized property. Just wanted to ruminate about possible alternative designs since I agree that the proposed signature of the init accessor is a little bit messy in its full generality.

  • I don't love this restriction:

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

    IMO the implementation detail of definite initialization shouldn't leak out into the decision of how to order properties in source. It's not self evident that this would be the "right" order in terms of documentation, organization. Could we just topologically order the properties for the purpose of DI and diagnose if there's a cycle in the accesses:? Seems like the cycles wouldn't even be an issue initially unless we did decide to allow init accessors for stored properties.

6 Likes

That's not really true. In Swift, declarations have all sorts of information in their signature about their invariants, such as the input values their types, the return type of the function, and any requirements on those types (e.g. in a where clause). It's crucial that this information is explicitly stated rather than inferred from the function implementation for all of the reasons that I mentioned. Localizing the effects of the code you write/change inside a function implementation (rather than propagating the effects out to all callers) leads to a much better programming experience because you get precise error messages at the point when you make a mistake. A key example of this is the error messages you get when using the C++ template system versus Swift's generics system; errors inside a generic function implementation that are independent of call sites are so much easier to understand and fix than making a mistake in a C++ template implementation, and not knowing about it until you get a pile of template instantiation diagnostics when you use the template you wrote. Stating the requirements of an API in the signature leads to better diagnostics both inside the implementation, and at the call-site, because the compiler has more information about the API contract.

There are other technical reasons related to the programming experience for localizing the impact of the code you change in an implementation, such as incremental build times (because code changed in an implementation only invalidates that function, there's no impact on any code that can use that function), but I firmly believe in the conceptual value of explicitly stating your intended invariants in a function signature, and having those invariants validated statically by the compiler.

The goal of the pitch is to generalize the bespoke definite initialization behavior of property wrappers to enable fully abstracting away storage using computed properties. This has proven to be a useful feature via property wrappers, but property wrappers have also proven to not be sufficient for many use cases that need custom transformations -- for example, wrappers that have attribute arguments that don't conceptually need to be stored (like @Lazy({/* code to initialize */}) or @Clamping(min: 0, max: 100)), enclosing-self property wrappers, or patterns that don't really fit into the existing property wrapper model like @Observable. This is where attached macros come in, which offer custom transformations to support these use cases, but macros fall short in initializers because transformed properties cannot be initialized in the same way post macro-expansion. I believe init accessors will be used commonly with macros, but I also think that being able to fully encapsulate the implementation mechanism of a computed property in its accessor list is a generally useful thing to do in plain Swift code.

init accessors can be used without setters. You might have a computed property that's backed by a stored let constant, so it has a getter but no setter. You'd be able to use init accessors to support assigning to the computed property in an initializer to initialize the let, but you won't be able to assign to that computed property after it's initialized. I can add an example of this in the proposal.

There is already an example in the proposal now where initialization would use different code from the property's setter. It's the @Obsevable example, but I don't think I explicitly wrote what the expanded code would look like. I can update that as well.

I didn't use key-paths because these aren't really values that will be evaluated as part of the call. Others have pointed out that the proposed syntax is confusing, and I'm considering some of the other suggestions in this thread.

9 Likes

An alternative I have not seen spelled out yet is to put accesses and initializes into the same family as mutating and nonmutating. With access modifiers like private(set) we are also already used to parameterize keywords at this location. This would also leave the opportunity to reuse these keywords in other places like function declarations.

  var a: Double {
    accesses(b) initializes(c) init { /* I think `initialValue` should be implicitly here like `newValue` in `set` */ }
    get { /*...*/ }
    set { /*...*/ }
  }

as an alternative to accesses and initializes we could also consider getting/gets and setting/sets.

  var a: Double {
    getting(b) setting(c) init { /* ... */ }
    get { /*...*/ }
    set { /*...*/ }
  }
4 Likes

I thought of this a few days ago, and I think the initialization dependencies in this pitch are most similar to self parameter modifiers as far as existing language concepts go, but unfortunately this syntax is trickier to implement in the parser because accessing(x) and initializing(x) would already be valid code inside a computed property getter implementation. It would require a fair amount lookahead in the parser to determine whether initializing(x) is a self modifier for an init accessor or a function call inside the getter implementation.

3 Likes

I'm a definite +1 on leaving this as a future direction, and will just say I'm not 100% sure it should ever be allowed. Even when the init and set bodies bear a syntactic similarity, this entire pitch is predicated on further teasing out the language's distinction between initialization and mutation—there's something that feels kind of C++-y about allowing you to 'reuse' the syntactic form of x = y from the set declaration but implicitly change the meaning of = within that expression to mean something different. With this pitch it could invoke two entirely different code paths!

Another question that came to me: IIRC we currently have a rule that property observers are not called for properties that are set within a type's init. Should this apply to properties which are set (not initialized) from an init accessor? E.g.,

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

  var y: Int {
    init(accesses: x) {
      x = newValue
    }
    get { ... }
    set { ... }
  }
}

(Is setting a mutable property from the accesses list even allowed?)

2 Likes

So I’m +1 for the idea but -1 on the proposed syntax. It seemed odd at first but the examples in the pitch make it seem incredibly useful.

I have to agree with others about the overloading of method naming syntax for the initialises and accesses stuff. Initially I thought they were method arguments and was wondering why they don’t use an array of key paths. I think some of the other suggestions that move this out of the init signature look a lot cleaner. My favourite so far is probably @michelf’s suggestion of mimicking the “where” syntax, as it doesn’t change the syntax too drastically from the proposal but uses a familiar syntax pattern rather than adding new, weird, and conflicting syntax

5 Likes

I personally found the Angle type very helpful as a concise, concrete example, while trying to wrap my head around the point of the proposal, so please don't take it out! Maybe swapping them so that radians is the stored property and degrees is the computed "helper" would remove most of the "distraction" :slight_smile:

6 Likes

Are there any plans to permit the init accessor as a protocol requirement for properties?

protocol Foo {
  var a: Bar { get }
  var b: Bar { init get }
}

If so, we might want to find a good syntax for this first and simply project it back.

Should the protocol's property be permitted to specify that it wants to initialize or access another property?

If that does not make much sense, then I don't see any justification for explicit accesses and initializes lists of properties for the init accessor. I think the compiler can scan those and infer that part implicitly.

3 Likes

I really like how this opens up another pinch of magic previously hardcoded to property wrappers! Like most people here, I'm mostly taking issue with the accesses part. Relevantly, the proposal has this restriction on synthesized memberwise initializers:

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

Why not take this idea to its logical extreme and say an init accessor is allowed to access all properties ordered before its definition? It's a natural ordering that I believe people would naturally aim towards for code style, and it would make dependency cycles inherently impossible.

Since the initializes list is then the only involved list of properties, one might even consider a less function-argument syntax for it, like init(initialValue) for _x, or even init for _x, defaulting to a certain name for the argument like set to newValue. Admittedly, it feels a little strange reusing for so far outside its usual context, but it's probably far enough to avoid confusion? init(initialValue) initializes _x feels redundant to the point of silliness (even with the function argument style).

1 Like

I've updated the proposal to specify initializes and accesses in the effects clause of the init accessor, after the (optional) parameter list. You can read the full updated proposal draft here.

9 Likes

That's a big improvement IMO. Still feels a little weird to me, but it's no longer a misleading sort of weird.

4 Likes

+1 overall on the feature. I'm sure it would make some of my property wrappers more ergonomic.

I don't think the attribute syntax works here (at least it would be a larger departure) since it is an essential part of the declaration and I like the updated syntax much better than the original proposal.

The use of parentheses feels weird to me, but from a formatting standpoint I don't hate it since parentheses are a great place to line-break.

Although a slightly different context, maybe throws could follow a similar pattern and support an optional constrained error type(s) in the future?

func helloUniverse() throws(InterplanetaryError) { ... }

EDIT: Minor clarifications

5 Likes

I agree that modelling these as effects as is now proposed makes much more conceptual sense than than the original proposal or my suggestion of an attribute. +1

1 Like