[Pitch] `init` accessors

The ad-hoc nature of property wrapper initializers mixed with an exact definite initialization pattern prevent property wrappers with additional arguments from being initialized out-of-line.

I’m not able to unpack the technical description here into concrete consequences. Could we have an example of a property wrapper with additional arguments, showing what is possible under this pitch and wasn’t before? I think the examples we already have are not of this character, unless I’m also misunderstanding that?

(I have a secret hope that this is hinting at ways to make @Clamped(min: 0, max: 100) play nicely with Decodable synthesis, but that’s probably me reading too optimistically into the bits I don’t understand.)

Is the term “out-of-line” defined somewhere? (I assume it’s a compiler-design term of art, but not knowing it makes evaluating the capabilities of the pitch much harder.)

4 Likes

I agree completely. This seems like it might be a very useful feature, but without examples of usage it’s very hard to understand what is being proposed.

Also: for who’s benefit are we stating what is being accessed or initialised? Presumably api consumers without access to the function code? But then why couldn’t the compiler just generate that? It knows what’s going on.

And about the syntax, is there a reason we can’t use keypaths here?

Finally: who on earth would implement Angle with degrees as the base format? Seems pretty unrealistic…

3 Likes

+1 on this. initialValue seems a better fit.

The syntax is a bit odd but it's aligned with the existing one for set.

struct T {
    
    var _value: Double
    
    var value: Int {
        get {
            return Int(_value)
        }
        set(intValue) {
            _value = Double(intValue)
        }
    }
    
}

Oh, thank you for reminding me that the set parameter override had totally different syntax. :person_facepalming:

I updated my above example to properly reflect that as:

var x: Int {
  init(initialValue) {
    print(readMe) // accesses
    self._x = initialValue // initializes
  }
  ...
}
2 Likes

I don’t find struct Angle compelling at all. Having written such a type myself, you can just factor out radian-to-degree conversion into a helper function and have init(radians:) directly initialize the concrete backing store.

I think the proposal would be improved by showing when that technique ceases to be feasible. Is it as soon as property wrappers are introduced, or only in more complicated situations?

3 Likes

In general, I think inferring information about init accessor signatures from the bodies of other accessors is a recipe for confusion, because you will no longer get useful diagnostics if you accidentally access something you didn't mean to in the body of an init accessor. But I also don't think it's possible, because getters and setters would use initializes and accesses dependencies in the same way, but the initialization semantics are completely different between those two dependencies. The former states that the init accessor subsumes the initialization of a stored property, and the latter states that the init accessor relies on the stored property already being initialized at the point when the accessor is called. This difference will change how the compiler treats an assignment in the type's initializer during definite initialization analysis, and because the other accessors would use those dependencies in the same way after all of self is initialized, it's not possible to infer.

Yeah, I think init accessors on computed properties in extensions should be banned. It might be possible to support this in extensions inside the defining module of the type, but I don't think that's a good idea because you'd be able to write an extension that breaks initializers that might be implemented in a completely different file. That doesn't sound like a great programming experience.

2 Likes

I should have kept my original snippet and used a name that was obviously a placeholder.

1 Like

This is a pretty nice proposal, and I like init as an accessor name. What I don't like is the initializes: part of the syntax that looks like an argument but really isn't.

I'd much prefer if the syntax for dependencies was expressed more like a type constraint clause:

struct S {
  private var _value: Wrapper<Int>
  var value: Int {
    init(newValue) initializing _value { // where-clause style syntax
      self._value = Wrapper(wrappedValue: newValue)
    }

    get { _value.wrappedValue }
    set { _value.wrappedValue = newValue }
  }
}

And I'd do the same for accesses:

struct ProposalViaDictionary {
  private var dictionary: [String: String] = [:]

  var title: String {
    init(newValue) accessing dictionary { // where-clause style syntax
      dictionary["title"] = newValue
    }

    get { dictionary["title"]! }
    set { dictionary["title"] = newValue }
  }
}

An interesting future direction with this dependency syntax is that it could be made to work with other accessors and methods, allowing them to be used before self is fully initialized. For instance:

struct Point {
  var x, y, z: Int

  init() {
    x = 1
    y = 2
    z = sum()
  }

  func sum() -> Int 
    accessing x, y // where-clause style syntax
  {
    return x + y
  }
}
10 Likes

This is an interesting observation. There are definitely cases where the init accessor and set accessor need to be separate functions, because the generated code is different for initialization vs setter calls of stored properties (which both look like assignments in source), so it wouldn't necessarily be using the same implementation "under the hood". If we were to support a convenient way to "re-use" your setter implementation for your init accessor, I think it should be explicit (rather than being the default), but I could imagine some kind of sugar to avoid having to write the code twice. I also think that could be added on top of this proposal in the future. Something like this suggestion below

2 Likes

I haven't thought through whether this makes sense / is feasible, but I think you could use the same syntax proposed here if init accessors were also supported for stored properties. You could imagine being able to add an accessor block just to implement an init accessor, similar to the syntax for stored property observers:

class MyView: UIView {
  let theme: Theme {
    init(initializes: label, button) {
      self.theme = newValue // this is a stored property, so the init accessor also needs to initializes this property
      self.label = Label(theme: theme)
      self.theme = Button(theme: theme)
    }
  }
  let label: Label
  let button: Button

  init(theme: Theme) {
    self.theme = theme // initializes 'theme', 'label', and 'button'
    super.init()
  }
}
3 Likes

This proposal does not change the existing property wrapper initialization semantics. This proposal only generalizes the language mechanism, and uses it for the existing property wrapper initialization behavior. It would be possible to extend the out-of-line initialization behavior of properties using this generalization in a future proposal, but I personally think customizing how custom attributes can transform properties and initialization of those properties is better accomplished using attached macros.

Yes, I was referring to this sort of pattern with property wrappers:

struct S {
  @Clamping(min: 0, max: 100) var value: Int // no initial value

  init(value: Int) {
    self.value = value // error
  }
}

I stole the term from the Out-of-line initialization of properties with wrappers section of SE-0258. I can link to this resource in the proposal.

2 Likes

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