Property Wrapper with Multiple Arguments

Can someone explain to me why we need to define a default value for wrappedValue when a property wrapper has arguments a consumer can provide? For example:

@propertyWrapper
struct Multiple<Value> {
    var wrappedValue: Value
    var argument: Int
}

struct Test {
    @Multiple(argument: 5) var test = ""
}

struct TestWithoutDefault {
    @Multiple(argument: 5) var test: String // Compiler Error: Missing argument for parameter 'wrappedValue' in call
}

Does anyone know why this is the case and would it be possible to have swift synthesis this properly in the future?

2 Likes

I don't understand the question. Multiple is a struct, therefore all its properties must be initialized when an instance is created. In your last example, no value is given, so the compiler emits an error.

If you changed it to

class TestWithoutDefault {
    var test: String
}

You would get an error for there being no initializer (note: I changed it to a class), and for the same reason -- there's nothing to give test a value when an instance of TestWithoutDefault is created.

1 Like

@Avi The problem here lies with some of the semantics when initializing a propertyWrapper without arguments. Take this example:

struct NoArguments {
    @State var hello: String
}

This example allows the user to differ initialization of the State property wrapper until the initialization of the NoArguments struct.

What I ultimately want to know is if it would be possible in future versions of swift to synthesize propertyWrappers as followed:

//User provided struct
struct Test {
    @Multiple(argument: 5) var test = ""
}

//Synthesized Result
struct Test {
    @Multiple var test: String
    init(test: String) {
        _test = Multiple(wrappedValue: test, argument: 5)
        //...
    }
}

Like Avi I don’t understand what you’re expecting to happen here.

Your property wrapper struct takes a non-optional wrappedValue - it can’t be initialised without a value. If you don’t provide one you get a compiler error as you can see.

If you want a property wrapper that doesn’t require an initial wrapper value like your @State example then you need to make wrappedValue optional.

I think I understand what @Andrew_Arnopoulos is getting at. I don't see any reason why the initial value of test needs to be provided inline with the declaration, since it will ultimately be provided in the synthesized initializer. I don't see anywhere he's trying to initialize an instance of Multiple without providing a wrappedValue. The fully-expanded example would be:

// User writes
struct Test {
  @Multiple(argument: 5) var test: String
}

// Compiler synthesizes:
struct Test {
  var _test: Multiple<String>
  var test: String {
    get { _test.wrappedValue }
    set { _test.wrappedValue = newValue }
  }

  init(test: String) {
    _test = Multiple(wrappedValue: test, argument: 5)
  }
}

Have I messed up the transformation for the property wrapper and/or the synthesized initializer somewhere?

6 Likes

Right - I’m with you now. Just to confirm - this works as expected if you have a non-optional var and a property wrapper with a non-optional wrappedValue and no additional arguments? (I think it does).

It does! And it works if argument is given an initial value in the declaration of Multiple.

This is exactly what I'm talking about. Thank you for doing a better job at explaining this than I did :smile:

1 Like

It looks like the compiler cannot initialise the wrapper, because the compiler "splices" the initialiser call to insert the initial wrapped value but in this case fails to do so and errors out.

From what I remember from working on some property wrapper implementation code, when one writes:

@Multiple(argument: 5) var test = ""

the initialiser for the property (which is ””) is rewritten into:

Multiple(wrappedValue: “”, argument: 5)

So, when you write @Multiple(argument: 5) var test, the initial wrapped value is missing and thus the error.

2 Likes

Yeah, that matches with my understanding of how the transformation works today for property wrappers with arguments. It still seems like the compiler should be able to look at a wrapped property like

@Multiple(argument: 5) var test: String

and realize that this is only a "partial" initialization, so we'll have to wait until the synthesized initializer to fully expand @Multiple(argument: 5) to Multiple(wrappedValue: test, argument: 5).

@Andrew_Arnopoulos, if you want to see this added to language the next move would be to start a thread in #evolution:discuss and file a ticket on bugs.swift.org. We'd have to consider whether the semantics you propose here are consistent with the existing usages on property wrappers and think about whether there are any corner cases that make this transformation unsound.

2 Likes

I understand. That seems like a reasonable extension to me. For compatibility, the compiler would still have to synthesize the version of init which takes the wrapper type, but it seems reasonable to have an overload that takes the property's type and synthesizes the code as in your example.

Sorry for the delay but here is the proposal in the evolution forum: