Allow Property Wrappers with Multiple Arguments to Defer Initialization when wrappedValue is not Specified

Hello Swift Community. I started a conversation about this on the Using Swift Forum and some of the people there thought it might be a good idea to pitch the idea here.

Allow Property Wrappers with Multiple Arguments to Defer Initialization when wrappedValue is not Specified

Introduction

Swift's Property Wrappers allow for wrappers without arguments to defer specifying the wrappedValue until the initialization of the containing type. This proposal adds this feature to Property Wrappers that have multiple arguments by extending the current initializer synthesization.

Motivation

In the current implementation of Swift if you create a value type with a Property Wrapper that only takes the wrappedValue argument you are able to defer the initialization of the wrappedValue until the initialization of the containing value type. Take the following example:

@propertyWrapper
struct NOOP {
    var wrappedValue: Int
}

struct Model {
    @NOOP var property: Int
}

Model(property: 5)

The code above will build and run just fine but the following code will not:

@propertyWrapper
struct Argument {
    var wrappedValue: Int
    var argument: String
}

struct Model {
    @Argument(argument: "hello") var property: Int //Error: Missing argument for parameter 'wrappedValue' in call
}

Model(property: 5) //Error: Cannot convert value of type 'Int' to expected argument type 'Argument'

You are able to get around this error by adding the following extension:

@propertyWrapper
struct Argument {
    var wrappedValue: Int
    var argument: String
}

struct Model {
    @Argument var property: Int
}

extension Model {
    init(property: Int) {
        _property = Argument(wrappedValue: property, argument: "hello")
    }
}

Model(property: 5)

This however has several draw backs:

  • Instead of defining Argument's additional property at the declaration site you the programmer are now doing it manually inside of a custom non-synthesized initializer. This increases the probability of bugs being introduced.
  • If an framework designer only provides a propertyWrapper they are not able to add these custom initializers for the consumers of their framework.

Proposed solution

I propose that we extend propertyWrapper's synthesized initializers to support propertyWrappers with multiple initializer arguments.

Source compatibility

This change is purely additive and should not affect source compatibility.

Effect on ABI stability

No known effect.

Effect on API resilience

No known effect.

28 Likes

+1 from me. It'll reduce a common pain point in using PropertyWrappers.

+1. Unless there’s some sort of technical limitation I’m not seeing, this seems like a great hole to patch up!

Super big +1, just do it!

Could you explain what the difference is between what you’re proposing and what is a library like CoreStore is already doing? Look at the property wrapper parameters in this release: https://github.com/JohnEstropia/CoreStore/releases/tag/7.1.0

The syntax discussed here already works when the wrapper type provides an initializer which doesn’t take a wrappedValue argument, since the appropriate initializer can be called without any initial value for the wrapped property.

Note that if you add an initial value, it does what you want:

struct Model {
    @Argument(argument: "hello") var property: Int  = 17 // okay!
}

Doug

1 Like

I think this comment from the initial discussion thread illustrates the proposed change quite well.

The entire point is to not require = 17 just to make it compile. Instead, synthesize an init that has property: Int as a parameter, just as the compiler would do if property were not wrapped by a property wrapper.

1 Like

I see there's some misunderstanding about the motivation for this pitch. Perhaps the following example will serve to illuminate:

@propertyWrapper
struct Wrapper {
    var wrappedValue: Int
    var projectedValue: String
}

struct S {
    @Wrapper(projectedValue: "three") var x: Int = 3

    // synthesized
    init(x: Wrapper) {
        self._x = x
    }

    // proposed
    // Allow: @Wrapper(otherValue: "three") var x: Int
    init(x: Int) {
        self._x = Wrapper(wrappedValue: x, projectedValue: "three")
    }
}

let s1 = S(x: Wrapper(wrappedValue: 4, projectedValue: "four"))
let s2 = S(x: 3)

print(s1.x, s1.$x) // 4, four
print(s2.x, s2.$x) // 3, three

The idea is to improve the ergonomics of the synthesized init, by accounting for required parameters of the wrapper which have been given in the property declaration.

Today, the initial value given to a wrapped property is rewritten by the compiler to be the value passed to wrappedValue of the wrapper's constructor. In addition, for wrappers with no other parameters, the compiler will synthesize an init which takes a parameter of the property's type, and delay the call to the wrapper's constructor to this synthesized init.

This pitch is aiming to combine these two features: Push down the construction of parameterized wrappers into an init, and have that init take a parameter of the property's base type, not its wrapper type.

7 Likes

+1, and FWIW I think this would be relatively straightforward to implement using the same strategy as out-of-line initialization for property wrappers without additional arguments :slightly_smiling_face:

3 Likes

That's actually a scenario that I hadn't considered but it does illustrate the point that this is about improving the ergonomics of property wrappers

+1

I think we have a general consensus that this would be a good change. I'm not sure what the threshold for putting up a PR for on the swift evolution repo but would anyone have any objections to me putting up a PR now for the core team?

You need to implement the feature first before you raise a pull request for the proposal.

1 Like

Would this feature also work well or even enable out of line (late) assignments / initialization for locally wrapped variables (not type properties)? (It‘s not supported yet, but it probably will and should in the future.)

@Argument(argument: "hello")
var foo: Int

foo = 42
1 Like
Terms of Service

Privacy Policy

Cookie Policy