[Pitch] `init` accessors

Hello, Swift Evolution!

Property wrappers have the ability to abstract the storage of a property, including bespoke definite initialization support:

@propertyWrapper
struct Wrapper<T> {
  var wrappedValue: T
}

struct S {
  @Wrapper var value: Int

  init(value: Int) {
    self.value = value  // Re-written to self._value = Wrapper(wrappedValue: value)
  }

  init(other: Int) {
    self._value = Wrapper(wrappedValue: other) // Okay, initializes storage '_value' directly
  }
}

@Douglas_Gregor and I wrote up a pitch to generalize the initialization behavior of property wrappers using a new kind of computed property accessor that contains custom initialization code. With this proposal, property wrappers have no bespoke definite initialization support. Instead, the desugaring includes an init accessor for wrapped properties:

@propertyWrapper
struct Wrapper<T> {
  var wrappedValue: T
}

struct S {
  private var _value: Wrapper<Int>
  var value: Int {
    init(newValue, initializes: _value) {
      self._value = Wrapper(wrappedValue: newValue)
    }

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

  init(value: Int) {
    self.value = value  // Calls 'init' accessor on 'value'
  }
}

This proposal enables custom out-of-line initialization for any property-wrapper-like pattern using computed properties, including via accessor macros.

Here is the full proposal for init accessors.

Please let us know your questions, thoughts, and other constructive feedback!

-Holly

28 Likes

The fact that the right hand side of the second initializer argument seems like a confusing way for value to communicate that accessing it initializes _value. It also doesn’t seem entirely necessary; the getter is only valid if _value has been initialized. Can’t control-flow analysis verify that init has definitively initialized all properties that get and set rely on being initialized?

3 Likes

My gut reaction to this is that it's weird, but it is solving an actual problem, the syntax mostly makes sense, and I'm broadly a fan of breaking property wrappers down to smaller constituent features.

I am not a fan of initializes: and accesses: looking like function parameters. accesses: feels less off; it's not quite a function parameter but it's kinda doing something similar in that it defines the init's inputs. initializes: though is defining the init's outputs in a place where Swift doesn't normally do that. I don't have any concrete suggestions for how to improve this, though. I don't think it'd be improved by slapping the word inout (or out) in there.

How would this work for computed properties defined in extensions? I'm guessing the answer is just that you can't use it in extensions? Anything else seems very complicated.

14 Likes

The use cases (specifically macros) might be rather complex to solve that - having explicit declarations of the read and initialization make solving that logically easier. I could imagine that if the solver could apply that (which I'm not sure it is generally solvable) that type of enhancement could be added later.

In the mean time attached macros would gain considerably easier use at minimal cost (since they have the proper information for synthesis).

Overall the pitch looks fantastic; I really think the struct Angle is a compelling example. For a use case Observation is a prime candidate; it will make initialization feel natural.

1 Like

This is really cool. Thanks for the Angle example, I agree with Philippe that it made a great case for this to be a general feature outside of more esoteric use cases like property wrappers.

In both the Angle and ProposalViaDictionary examples, the body of the init accessor is identical to the set accessor. For such cases, is there anything preventing us from letting the user elide the body of the init and just use the set under the hood, and leave writing an explicit init body for cases where they have to differ like the property wrapper case?

5 Likes

It's cool but the pitched syntax is extremely strange.

  1. I would not force newValue bacause that's not a setter where the value changes. Personally I would prefer value or initialValue to make it clearer.

  2. I really don't see why the developer has jump through these unusual syntactic grammar requirements:

struct S {
  var readMe: String

  var _x: Int

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

    get { _x }
    set { _x = newValue }
  }
}

We could rather mimic the set accessor here.


struct S {
  var readMe: String

  var _x: Int

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

    get { _x }
    set { _x = newValue }
  }
}

Just the plain init(initialValue) { ... } could be sufficient enough. I mean, why can't the compiler not figure out which stored properties are actually being initialized from the init accessor and which are read?

That makes the syntax much simpler and aligned. On top of that the initializes and accesses becomes fully transparent.

7 Likes

I would prefer that we don't overload argument or closure syntax but I do see value asking the developer to explicitly state what is accessed and what is initialized.

Is there a reasonable way to use property wrapper-like syntax for this?


@propertyWrapper(accesses: readMe, initializes: _x)
struct S {
  var readMe: String

  var _x: Int

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

    get { _x }
    set { _x = newValue }
  }
}
1 Like

It would be nice if this same concept could be applied outside of property wrappers/computed vars to lets that access properties of self in their initialization. I've found that this can be common in code using UIKit, where a dependency might be needed in multiple vars of the same class, and those dependencies might be quite complex to configure.

One suggestion that I have to facilitate that is to consider making accesses similar to a if-let binding: scoped to a block. Which might look like the following:

class MyView: UIView {
    let theme: Theme
    init(theme: Theme) {
        self.theme = theme
        super.init()
    }
    accesses(theme) {
         let label = Label(theme: theme)
         let button = Button(theme: theme)
    }
}

It would be possible to nest these "access scopes", which would make it would be very clear when a dependency cycle had occurred.

I see two major disadvantages with my suggestion: first, the nesting might make it hard to tell that label and button are actually instance vars of MyView and not part of some other function. Secondly; this suggestion makes accesses and initializes potentially inconsistent.

If I’m understanding the pitch correctly, the newValue parameter is optional (like it is for set) so the Angle example could be:

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

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

  // …
}

If the body of set and init are the same, maybe explicitly aliasing them or defining them together would be an option?

Eg

Aliasing:

  var radians: Double {
    init(initializes: degrees) = set

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

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

Defining together:

  var radians: Double {
    init(initializes: degrees)
    set {
      degrees = newValue * 180 / .pi
    }

    get { degrees * .pi / 180 }
  }

These could be Future Directions though.

6 Likes

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.)

3 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…

2 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
  }
}
8 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