Skip to content

Conversation

@xedin
Copy link
Contributor

@xedin xedin commented Aug 2, 2022

Initial implementation of @typeWrapper attribute which could be used to declare a type
to "wrap" other types by routing all their stored property accesses through itself.

The type wrapper is very similar to a property wrapper but there are couple of fundamental
differences:

  • Could only be associated with a class or a struct;
  • Disables memberwise and default initializers, produces a special synthesized initializer that covers all the stored properties instead;
  • All access to the stored properties is routed through a single instance of a type wrapper;
  • A per-instance $Storage container allocates storage for all of the wrapped properties;
  • Default initialization expressions are moved to the synthesized constructor;
  • Access is performed via subscripts that accept key paths to the underlying storage.

Valid type wrapper has to have:

  • A single generic parameter (i.e. Storage) which would be substituted with storage type;
  • init(memberwise: Storage) - initializer that accepts a generic parameter of underlying storage;
  • subscript<Value>(storageKeyPath path: {Writable, ReferenceWritable}KeyPath<Storage, Value>) -> Value -
    a subscript that would be called on every applicable property access.

Here is an example of a valid type wrapper declaration:

@typeWrapper
struct Wrapper<Storage> {
  var storage: Storage

  init(memberwise: Storage) {
    self.storage = memberwise 
  }

  subscript<Value>(storageKeyPath path: WritableKeyPath<Storage, Value>) -> Value {
    get { storage[keyPath: path] }
    set { storage[keyPath: path] = newValue }
  }
}

Now, let's see Wrapper in action to appreciate how much boiler place it removes:

@Wrapper
struct Person {
  var name: String
  var age: Int
}

Access to name and age of a Person is going to get routed through Wrapper type.

The transformed type looks like this:

struct Person {
  struct Storage {
    var name: String
    var age: Int
  }

  var _storage: Wrapper<Storage>

  init(name: String, age: Int) {
    self._storage = Wrapper(memberwise: Storage(name: name, age: age))
  }

  var name: String {
    get { _storage[storageKeyPath: \Storage.name] }
    set { _storage[storageKeyPath: \Storage.name] = newValue }
  }

  var age: Int {
    get { _storage[storageKeyPath: \Storage.age] }
    set { _storage[storageKeyPath: \Storage.age] = newValue }
  }
}

All of the boilerplate code required to create and maintain 1-1 storage between type and Storage and proper initialization is now handled by the compiler.

Open questions:

  • Support for pre-existing user-defined initializers
  • ✅ Support for stored properties with property wrappers

@xedin xedin requested a review from hborla August 2, 2022 00:04
@LucianoPAlmeida
Copy link
Contributor

Hey @xedin,
One question, consider this

@Wrapper
struct Person {
  var name: String
  let age: Int = 0
}

Since wrapper subscript is defined as WritableKeyPath and defines a set which cannot be applicable to a let property what would be the expected behavior in this case?

@xedin
Copy link
Contributor Author

xedin commented Aug 2, 2022

@LucianoPAlmeida properties like that are not currently wrapped.

@xedin
Copy link
Contributor Author

xedin commented Aug 2, 2022

A more interesting example is the one which uses a class as a let e.g.:

class X {
  var v: Int = 42
}

struct S {
  let x: X = X()
}

Since x is a reference type it would still be modifiable e.g.:

var s = S()
s.x.v = 0

I think this case would have to be wrapped but it would have to pick different overloads of subscript from type wrapper type...

@LucianoPAlmeida
Copy link
Contributor

I think this case would have to be wrapped but it would have to pick different overloads of subscript from type wrapper type...

Yeah, from my understanding an overload of subscript that takes an WritableKeyPath would not work in that let reference case, so user would have to implement a ReferenceWritableKeyPath overload of the subscript.

let wkp: WritableKeyPath<X, Int> = \.v
let rwkp: ReferenceWritableKeyPath<X, Int> = \.v
let s = S()
s.x[keyPath: wkp] = 0 // error: cannot assign through subscript: 'x' is a let constant
s.x[keyPath: rwkp] = 0 // ok

Is that what you mean by different overload?

@xedin
Copy link
Contributor Author

xedin commented Aug 3, 2022

Is that what you mean by different overload?

Yes, it's currently modeled as implicitly generated AST so type wrapper could define multiple overloads of subscript and type-checker would just pick one that works for synthesized code.

@LucianoPAlmeida
Copy link
Contributor

Yes, it's currently modeled as implicitly generated AST so type wrapper could define multiple overloads of subscript and type-checker would just pick one that works for synthesized code.

Ah got it, Thanks!

@xedin
Copy link
Contributor Author

xedin commented Aug 12, 2022

@swift-ci please clean test

@xedin
Copy link
Contributor Author

xedin commented Aug 17, 2022

@swift-ci please clean test

@xedin
Copy link
Contributor Author

xedin commented Aug 17, 2022

@swift-ci please clean test

@xedin
Copy link
Contributor Author

xedin commented Aug 18, 2022

@swift-ci please clean test Linux platform

@xedin
Copy link
Contributor Author

xedin commented Aug 18, 2022

@swift-ci please test macOS platform

@xedin
Copy link
Contributor Author

xedin commented Aug 18, 2022

@swift-ci please test Windows platform

@xedin
Copy link
Contributor Author

xedin commented Aug 22, 2022

@swift-ci please test

@xedin
Copy link
Contributor Author

xedin commented Aug 23, 2022

@swift-ci please test macOS platform

@xedin xedin force-pushed the type-wrappers branch 2 times, most recently from 2afa6cb to d5d4338 Compare August 24, 2022 21:21
xedin added 8 commits August 26, 2022 12:25
`$Storage` type is going to used as a type of `$_storage` variable
and contains all of the stored properties of the type it's attached
to.
This is the property that would get used to route stored property
accesses through a type wrapper.
…pper property

Given a stored property associated with a type wrapped type,
produce a property that mirrors it in the type wrapper context.
…ped type

A getter routes accesses through a subscript of `$_storage` variable
injected into a wrapped type.
…ped type

A setter routes assignment through a subscript of `$_storage` variable
injected into a wrapped type - `$_storage[storageKeyPath: \$Storage.<property>] = newValue`
xedin added 19 commits August 26, 2022 12:25
Compiler cannot synthesize regular memberwise or default
initializers for type wrapped types because stored properties
of such a type cannot be accessed directly.

A special initializer would be synthesized instead, it is going
to initialize `$_storage` variable and handle default initialization
of stored properties.
Synthesize an `init` declaration which is going to initialize
type wrapper instance property `$_storage` via user provided
values for all stored properties.
This is important because we need to force existance of the
underlying storage at the right moment.
This is important because we need to force existance of the
underlying storage at the right moment.
… vars are not transparent

Type wrapped variables loose their storage and are accessed through
`$Storage` and synthesized accessors cannot be transparent.
All of the stored properties are wrapped which means that their
initializers are subsummed and moved to the synthesized `init`
as default arguments.
…roperty wrappers

Type wrapper doesn't wrap anything expect to a private backing storage property.
This means that type wrapper is applied first and subsequent `.wrappedValue` and/or
`.projectedValue` is referenced for the returned property wrapper type.
…members

Since properties with property wrappers are not supported, default
init synthesis needs to handle them as well by using correct interface
type depending on outer wrapper capabilities and setting correct
default expression.
…ed memberwise `init`

If a stored property would be in a default memberwise initializer
it would be added to the `init` synthesized for a type wrapped type
as well i.e. a `let` property without a default.
@xedin
Copy link
Contributor Author

xedin commented Aug 26, 2022

@swift-ci please test

@xedin xedin merged commit e22fc3e into swiftlang:main Aug 29, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants