[Pitch] `init` accessors

Can't those things move even further and become more transparent?

init(initialValue) { [&foo, bar] in // accesses `bar`, initializes `foo`?
  ...
}

I'd like to point out again that newValue makes more sense for set as the value is potentially mutating multiple times. initialValue does seem a better fit for the init accessor. We have a precedence naming scheme here from PWs as both wrappedValue and projectedValue pointing out the proper contextual meaning.

6 Likes

I know this is only for computed properties, but does it make sense to think about how this would look if accesses were a general feature for initializing instance variables in a dependency order? I often find I want write code like the second example to avoid cluttering the initializer or using lazy. It feels like we are halfway there by adding this for computed properties.

struct S {
 var text: String = "Hello World"
 var button: Button {
   init() accesses(text) { button = Button(label: text) }
 }
}
struct S {
  var text: String = "Hello World"
  var button = Button(label: text) // sort of an implicit `accesses(text)`
}

Or a variation with the capture list suggested by others:

struct S {
  var text: String = "Hello World"
  var button: Button = { [text] in return Button(label: text) }()
}

Strong +1 on the idea!

Would definitely like to see the syntax refined, however. The 'initializes' and 'accesses' being within the parameter list feels at odds with syntax elsewhere in the language.

However, I'm a strong +1 on tis direction:

I think the capture list closely matches what I would expect from the closure style, and the ability to omit the initialValue parameter, makes it feel closer to get and set.

Maybe some bike-shedding on the capture list specifiers: perhaps defines or provides for the 'initializing' param list items, and maybe updates, inout or no specifier at all for the 'accessing' param list items.

private var _value: Wrapper<Int>
var value: Int {
  // perhaps 'defines' for the 'initializing' capture specifier
  // maybe 'updates', 'inout' or no specifier at all for the 'accessing' var
  init { [defines _value] in
    self._value = Wrapper(wrappedValue: initialValue)
  }
  get { _value.wrappedValue }
  set { _value.wrappedValue = newValue }
}

EDIT: Perhaps it's true that an 'accessing' captured var doesn't need a capture list specifier at all – much like a strong reference in a typical closure. I think this makes sense to me as it has a similar access pattern.

2 Likes

Any reason the attribute couldn't go on the outside?

struct S {
  private var _value: Wrapper<Int>

  @initializes(_value)
  var value: Int {
    init { _value = Wrapper(wrappedValue: initialValue) }
    get { _value.wrappedValue }
    set { _value.wrappedValue = newValue }
  }
}

// or

struct S {
  @initializedBy(value)
  private var _value: Wrapper<Int>

  var value: Int {
    init { _value = Wrapper(wrappedValue: initialValue) }
    get { _value.wrappedValue }
    set { _value.wrappedValue = newValue }
  }
}

I like the second version because it documents where it is configured.

3 Likes

I like that, too. Perhaps with variants for the three different possibilities:

  • initializing AND accessing: @initializes(_member1, accessing: _member2)
  • initializing ONLY: @initializes(_member1)
  • accessing ONLY: @initializes(accessing: _member2)

And stretches to multiple items in the list:

  • @initializes(_member1, _member3, accessing: _member2, _member4)

I think it would definitely have to sit with the var that has the accessor though.

struct S {
  private var _value: Wrapper<Int>

  @initializes(_value)
  var value: Int {
    init { _value = Wrapper(wrappedValue: initialValue) }
    ...
  }
}
1 Like

Maybe another variation that does a two-stage initialization instead of using accessing.

struct S {
  @initializeFirst 
  private var readMe: String

  @initializedBy(value) 
  private var _value: Wrapper<Int>

  // Allowed to access any property marked as `@initializeFirst`.
  var value: Int {
    init { 
      print(readMe)
      _value = Wrapper(wrappedValue: initialValue) 
    }
    get { _value.wrappedValue }
    set { _value.wrappedValue = newValue }
  }
}

I like the idea, but I'm wondering if those qualifiers need to be limited to the initializer or if they couldn't be extended to a dependency declaration of the property itself.

So instead of declaring that the initializer initializes another property, the property itself could declare that it writes another property:

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

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

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

This would also solve the duplication issue if the init and set are the same. The rule would be that if writes or accesses is declared on a property, and init is not defined, the setter is used as init:

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

The dictionary example would be considerably shorter if the separate init could be left out:

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

  var title accesses(dictionary): String {
    get { dictionary["title"]! }
    set { dictionary["title"] = newValue }
  }

   var text accesses(dictionary): String {
    get { dictionary["text"]! }
    set { dictionary["text"] = newValue }
  }
}

The general rule would be that as soon as accesses or writes are defined on a property, all of get, set and init would need to adhere to the declaration, i.e. the implementations would only be allowed to access or write other properties that are declared on the outer property.

3 Likes

I like this idea. Although more useful for computed properties, it might be nice to allow normal properties to take advantage of this too. This syntax would allow that. For instance, initializing a timestamp in another property.

+1 on the default init being set if no init accessor provided. I think I prefer the @initializing attribute, though.

struct Angle {
  var degrees: Double
  @initializing(degrees)
  var radians: Double {
    get { degrees * .pi / 180 }
    set { degrees = newValue * 180 / .pi }
  }
}
2 Likes

How does this idea compare to val-lang's set parameter decorator? Functions and methods - Language tour

Could this pitch be extended to more generally support initializing parameters to match val's set?

func foo(x: initializing Int) {
	x = 10
}

func main() {
	var x: Int
	foo(&x)
	print(x)
}

The reason I think that the dependency declarations should be part of the type signature is because they declare implicit parameters of the property (think of the self parameter of Pythons class methods).

It's probably more obvious if this feature was extended to functions, as suggested by @michelf.

class Point {
    var width: Int {
        didSet { updateArea() }
    }

    var height: Int {
        didSet { updateArea() }
    }

    var area: Int

    init(width: Int, height: Int) {
        self.width = width
        self.height = height
        updateArea()
    }

    func updateArea() writes(area) accesses(width, height) {
        area = width * height
    }
}

In this example the compiler would check that all paths in updateArea write the area property exactly once.

The pattern that one wants to run the same update code in a didSet block and in an initializer comes up quite often. At the moment a common workaround is to initialize the property with an invalid default value (e.g. var area = 0), or making the property optional if there is no default value available.

I'm wondering though if accesses and writes are specific enough as declarations, or if actually three declarations would be needed: initializes, updates, reads. The function would then look like this:

    func updateArea() initializes(area) updates(area) reads(width, height) {
        area = width * height
    }

writes could be a shortcut for initializes & updates, and accesses would be a shortcut for reads and updates. So the simplified version would look like this:

    func updateArea() writes(area) reads(width, height) {
        area = width * height
    }

That's a great idea, too.

To be clear, I think the idea is really good – I'm just a little adverse to introducing Yet Anotherā„¢ļø declaration style. It would still be part of the function signature. I think attributes fill this purpose well in Swift and will help to give some syntactic consistency. Just my opinion.

class Point {

    // probably needs marking, too
    var width: Int {
        didSet { updateArea() }
    }

    // probably needs marking, too
    var height: Int {
        didSet { updateArea() }
    }

    var area: Int

    init(width: Int, height: Int) {
        self.width = width
        self.height = height
        updateArea()
    }

    @initializing(area, accesses: width, height)
    func updateArea() {
        area = width * height
    }
}

My guess though, is that each function/accessor will need to be marked in order for the compiler to efficiently pick up on the initilalizer's dependencies. i.e. width and height accessors would need marking also in this example.

1 Like

Yeah, maybe you are right.

I still think this feature would be valuable without focussing on initializing only though. It could often be useful for optimizations and documentation to have compiler checked annotations of the dependencies of a property or function I think.

For the attribute syntax I would prefer this:

    @initializes(area)
    @updates(area)
    @reads(width, height)
    func updateArea() {
        area = width * height
    }


    // shortcut version
    @writes(area)
    @reads(width, height)
    func updateArea() {
        area = width * height
    }

IIUC, the compiler only needs to know:

  1. If a property will be assigned (initialised)
  2. If a property will be accessed (read OR write)

Adding anything else would likely be premature optimisation for a feature yet to be conceived.

Having said that, I think placing the the attribute with the declaration of the computed var, i.e. @initializing(x) var prop: { ... }), rather than with the accessor, var prop: { init initializing(x) { ... } }, provides good flexibility for future directions.

An alternative marker, might be:

@initializes(area, using: width, height)

We need something, especially since the new @Observable macro throws these non-initialized errors and breaks many property wrappers in the process, including the @Injected property wrapper types used in Factory and other dependency injection systems.

@Observable
class SplashScreenViewModel {
    @Injected(\.loginService) private var loginService: LoginServices
    ...
}

In Factory, the keypath initializer points to a DI container/factory that will provide the requested service. An initialized variable isn't needed (or even possible).

The @Observable macro basically breaks any property wrapper like this one whose job it is to pull a keyed value from a container, database, or other backing store.

You can "fix" the problem with @ObservationIgnored, but that needs to be done every single time a property wrapper is used, which in turn greatly discourages their use.

@Observable
class SplashScreenViewModel {
    @ObservationIgnored @Injected(\.loginService) private var loginService: LoginServices
    ...
}

It would be better if, as mentioned in this proposal, there was a away to mark the property wrapper itself as fulfilling any needed requirements.

1 Like