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.

47 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: Release Swift 5.2 / Xcode 11.4, New `Field` PropertyWrappers · JohnEstropia/CoreStore · GitHub

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.

4 Likes

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.

12 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:

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

2 Likes

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
2 Likes

I think that the existing mechanism for out-of-line property wrapper initialization already enables this. This proposed extension to allow out-of-line initialization of property wrappers with arguments in the wrapper attribute (in the absence of an initial wrapped value) can be implemented using the same mechanism, so this wouldn't be any different than:

func test() {
  @Wrapper
  var value: Int

  value = 10
}

Essentially, the compiler creates a placeholder for the wrappedValue argument to the backing property wrapper initializer that can be replaced with an actual value later on when the value is set. The compiler already knows how to decide whether value = 10 is supposed to be initialization or re-assignment; we'd probably just need to tweak those rules to allow this functionality for local wrapped variables.

4 Likes

One of the use-cases this would work nicely for is Codable wrappers if the wrapper has other properties you want to specify inline, but would only be initialized when an auto-synthesized init(from decoder:) assigns a value to the property, a simplistic example to illustrate would be (Formatted is a made-up wrapper here):

struct MyType: Decodable {
   @Formatted("hh:mm") var minutes: Date
   @Formatted("yyyy-MM") var months: Date
}

where you cannot assign a single date decoding strategy to a decoder.

This was something I was looking into before, but couldn't solve it without manually spelling out a decoding initializer.

10 Likes

Dream of getting it in Swift 5.4 :)

I started using property wrappers about a week ago and really like them, and I couldn't figure out how to get this particular aspect of them working and I figured there was something I was definitely misunderstanding. I was about to launch a detailed question on StackOverflow about this when my search results took me to this exact page, so I guess I'm not missing anything after all! I would definitely like to have wrappers allow for this basic functionality: a wrapper that takes additional arguments but does not require a default value to get the same initialization semantics as normal properties.

2 Likes

Is there any way to get some movement on this? I'd love to see this improvement too - I was reminded of this limitation today whilst working on my own encoding/decoding library.

2 Likes