Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to compose swift property wrappers?

I have recently been experimenting with swift property wrappers and wondered if there was any way of composing them together in order to achieve a more modular architecture. E.g:

@WrapperOne @WrapperTwo var foo: T

Looking through the documentation yielded nothing. The only reference to how to do this is on this GitHub page (https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md) (quotes below) which seems to say it is possible. Other articles have said that they are difficult to compose but have not explained how to go about doing this. However, I can't make heads or tails of it and would appreciate it if someone could show me some example code on how to implement this (see bottom of post).

When multiple property wrappers are provided for a given property, the wrappers are composed together to get both effects. For example, consider the composition of DelayedMutable and Copying:

@DelayedMutable @Copying var path: UIBezierPath

Here, we have a property for which we can delay initialization until later. When we do set a value, it will be copied via NSCopying's copy method. Composition is implemented by nesting later wrapper types inside earlier wrapper types, where the innermost nested type is the original property's type. For the example above, the backing storage will be of type DelayedMutable<Copying<UIBezierPath>> and the synthesized getter/setter for path will look through both levels of .wrappedValue:

private var _path: DelayedMutable<Copying<UIBezierPath>> = .init()
var path: UIBezierPath {
    get { return _path.wrappedValue.wrappedValue }
    set { _path.wrappedValue.wrappedValue = newValue }
}

Note that this design means that property wrapper composition is not commutative, because the order of the attributes affects how the nesting is performed: @DelayedMutable @Copying var path1: UIBezierPath // _path1 has type DelayedMutable> @Copying @DelayedMutable var path2: UIBezierPath // error: _path2 has ill-formed type Copying> In this case, the type checker prevents the second ordering, because DelayedMutable does not conform to the NSCopying protocol. This won't always be the case: some semantically-bad compositions won't necessarily by caught by the type system. Alternatives to this approach to composition are presented in "Alternatives considered."

Ideally, I would like to implement something like the following:

@propertyWrapper
struct Doubled {
    var number: Int
    var wrappedValue: Int {
        get { (value * 2) }
        set { value = Int(newValue / 2) }
    }
    init(wrappedValue: Int) {
        self.number = wrappedValue
    }
}

and

@propertyWrapper
struct Tripled {
    var number: Int
    var wrappedValue: Int {
        get { (value * 3) }
        set { value = Int(newValue / 3) }
    }
    init(wrappedValue: Int) {
        self.number = wrappedValue
    }
}

so that this could be achieved:

@Tripled @Doubled var number = 5

I understand that this example is a somewhat silly reason to implement property wrapper composition but this is merely for simplicity's sake when learning a new feature.

Any help would be greatly appreciated.

like image 264
Ben Avatar asked Jan 14 '20 17:01

Ben


People also ask

How do you create a property wrapper in Swift?

To make our wrapper configurable, we need to add all configuration parameters through an initializer. If the initializer contains a wrappedValue attribute (the initial value of our property), it must be the first parameter.

What is a property wrapper Swift?

A property wrapper adds a layer of separation between code that manages how a property is stored and the code that defines a property. For example, if you have properties that provide thread-safety checks or store their underlying data in a database, you have to write that code on every property.

What is wrappedValue in Swift?

A projection of the binding value that returns a binding. Returns a binding to the resulting value of a given key path.

What are property observers in Swift?

Property observers observe and respond to changes in a property's value. Property observers are called every time a property's value is set, even if the new value is the same as the property's current value .


2 Answers

As of Swift 5.2, nested property wrappers have become a lot more stable, but they're still a bit difficult to work with. I've written an article here about it, but the trick is that since the outer wrapper's wrappedValue is the type of the inner wrapper, and the inner wrapper's wrappedValue is the direct property type, you have to make the wrapper operate on both types.

The basic idea I've followed is to create a protocol which the wrappers operate on. You can then have other wrappers conform to the protocol as well, in order to enable nesting.

For example, in the case of Doubled:

protocol Doublable {
    func doubling() -> Self
    func halving() -> Self
}

@propertyWrapper
struct Doubled<T: Doublable> {
    var number: T
    var wrappedValue: T {
        get { number.doubling() }
        set { number = newValue.halving() }
    }
    init(wrappedValue: T) {
        self.number = wrappedValue
    }
}

extension Int: Doublable {
    func doubling() -> Int {
        return self * 2
    }

    func halving() -> Int {
        return Int(self / 2)
    }
}

extension Doubled: Doublable {
    func doubling() -> Self {
        return Doubled(wrappedValue: self.wrappedValue)
    }

    func halving() -> Self {
        return Doubled(wrappedValue: self.wrappedValue)
    }
}

struct Test {
    @Doubled @Doubled var value: Int = 10
}

var test = Test()
print(test.value) // prints 40

You could do the same thing for Tripled, with a Tripleable protocol, and so on.

However, I should note that instead of nesting @Tripled @Doubled, it might be better to create another wrapper like @Multiple(6) instead: then you won't have to deal with any protocols, but you'll get the same effect.

like image 192
Noah Gilmore Avatar answered Oct 01 '22 17:10

Noah Gilmore


There were some issues with using multiple property wrappers so that part of the feature was pulled out of Swift 5.1 but it will be available in 5.2. Until then you can’t use multiple property wrappers directly like this.

like image 23
bscothern Avatar answered Oct 01 '22 17:10

bscothern