[Pitch #3] SE-0293: Extend Property Wrappers to Function and Closure Parameters

Should this work in synthesized memberwise initializers too? So you can do:

struct Foo {
  @Traceable var value: Int
}

let history: History<Int> = ...
var foo1 = Foo(value: 10)
var foo2 = Foo($value: history)
1 Like

Yeah, I think that could work.

EDIT: That could work as a future direction.

I do think that property wrappers in patterns would be useful, but I agree with Chris that this is an orthogonal feature to this proposal. I also don't think this direction should apply to closure parameters, because today there is no precedent in the language for treating closure parameters as patterns. I personally think that designing closures to behave the same way as unapplied function references simplifies the mental model for property-wrapper parameters.

I've clarified the behavior of this syntax with composition in the proposal:

Since property-wrapper projections are not composed, this syntax will only infer one property-wrapper attribute. To use property-wrapper composition, the attributes must always be explicitly written.

Thank you for the suggestion! I've split up these two sections and added a bit more explanation/examples to start, but I'll continue working on clarifications here.

I'm writing up justification for the init(projectedValue:) design decision vs alternatives - will let ya'll know when it's in the proposal draft! EDIT: In the meantime, here's an excerpt from my brainstorming notes:

I have yet to see any concrete evidence that passing the backing wrapper directly is actually important, but this design does not prevent any future enhancement to support this. All of the use cases involve passing either a wrapped value or a projection around (i.e., the parts of the wrapper that are not implementation detail).

1 Like

Thanks for the hard work! I think this has become a pretty solid feature. I agree with most of the proposal, with a few minor points:


If the outermost property wrapper defines a projectedValue property, a local computed property representing the outermost projectedValue will be synthesized and named per the original parameter name prefixed with a dollar sign ( $ ).

This doesn't seem to handle mutating get projectedValue. Though that paragraph is already overlong, and it's quite a minor scenario, so :woman_shrugging:.


Does this also allow unapplied function reference with function arguments, i.e., log(value:) is allowed and is interpreted as log? This is heavily implied by UFR section, but I couldn't find an explicit example in the proposal.


Property wrappers on function parameters must support init(wrappedValue:) .

Rationale : This is an artificial limitation to prevent programmers from writing functions with an argument label that cannot be used to call or reference the function.

Maybe we can drop this requirement. We don't seem to have any problem with unusable function:

func foo(_: Never) { } // ok...

init(projectedValue:) is a new initializer here. Have you considered adding this back to the original property wrapper?

var projected: Binding = ...
@Binding var $value = projected

Aside from consistency's sake, I couldn't find a scenario where this could be useful, so maybe we don't need to add it (even as a future direction). It's out-of-scope for this pitch anyway.


Do you also plan to include use this feature for synthesized memberwise initializer? My impression is that it's left for future direction, but @filip-sakel's reply seems to imply otherwise:

Or maybe I just misinterpreted the discussion.


Property-wrapper parameters with arguments in the wrapper attribute cannot be passed a projected value.

What about an empty argument?

func foo(@Wrapper() a: Int) { }

This could be an interesting way to disable pass-by-projected-value. I think we should also disallow foo($a) here.

1 Like

I second this. I only quickly scanned the proposal and noticed this odd feature extension.

The original PW functionality does not and probably cannot support this feature. Let me try to explain.

What this adds is basically a convenience compiler sugar to opt into calling init(projectedValue:) which cannot be a requirement for a general PW.

It is not possible to design a PW that only has a projected value as a wrapped value is always a minimal requirement for the compiler to know which type a PW will wrap. A wrappedValue cannot be hidden as we won‘t be able to tell which type we would wrap. Therefore a projectedValue is only an extension of some PWs.

Initializing from init(projectedValue:) will still require you to either initialize wrappedValue directly or to implement at least a computed version of it.

So what are we really trying to solve here? To me it seems that we just want to add the ability to use the $ in even more places without adding much functionality to it.

I personally think the correct solution for this problem would be a simple one:

  • split the PW which you‘d otherwise extend with init(projectedValue:) into two separate PWs
  • provide multiple necessary overloads on functions where the proposal uses $ for parameter labels

To be clear, I'm in favor of adding init(projectedValue:) for function argument wrapper, but I'm not sure if we want to add init(projectedValue:) to original property wrapper.

For function argument, it'd be extremely useful for those I called shared storage wrapper that I found in my last survey:

but it doesn't do much for the original PW. size-wise, they don't differ too much from custom init:

$value = projectedValue
_value = .init(projectedValue: projectedValue)

though it may allow for inlined init(projectedValue:)

@Wrapper var $value = projectedValue

I think it's fine to have the initializer be optional, just like init(wrappedValue:). If you define it, the type must match the projectedValue, and you can then use var $value = ....

The first one probably wouldn't work since we'd just be shifting the need for init(projectedValue:) into one of the two PWs.

The second one would work for original PW, but I don't see how that could improve function argument wrapper.

1 Like

I understand that there could be ways to extend the general PW functionality with that, but I would to be careful not only for that but even for the current proposal.

How would you handle this case?

@propertyWrapper
struct W<A, B> {
  var wrappedValue: A?
  var projectedValue: B?
  init(wrappedValue: A) { self.wrappedValue = wrappedValue }
  init(projectedValue: B) { self.projectedValue = projectedValue }
}

func foo<A, B>(@W<A, B> bar: A, b: B.Type) { ... }

// compiler infers B as Int, but how would you tell it what A is?
foo($bar: 42, b: Int.self)

Regarding the $_ prefix for unlabeled parameters. I think it should be safe to drop the underscore as you could theoretically write baz($: 1, $: 2) and let the compiler figure out the parameters via the parameter order rules. Since the $ is reserved for identifiers, I don‘t see any issues or need to add an explicit underscore.

1 Like

You're right - the compiler can only synthesize a local projection property if the getter is nonmutating. I've clarified this in the text.

Yes, this is supported. I've added the log(value:) example to the section to demonstrate this.

That's a good point. I don't feel too strongly about this restriction. I was mildly concerned that adding a wrapper attribute to an existing parameter would be a source breaking change, but for libraries (with evolution) this is a moot point anyway because that would break ABI.

Yes, in the form of definite initialization - I intend to implement property wrapper initialization from a projected value via DI sometime in the future. I've added this as a future direction to the proposal.

Yeah, it's in future directions at the moment, but there's no reason not do to this out of the gate. In the previous design, this was a source breaking change for code that curried or referenced a synthesized memberwise init, but this is no longer the case because we've changed the type of the init in this design. I can add this to the proposal proper if we want this functionality to work out of the box - it's straightforward to implement.

Oh, that's kinda neat! This Just Works in the implementation :slightly_smiling_face:

Side note: I'm still implementing the transformation for function expressions, but I plan to have a toolchain for folks to try out sometime in the coming week.

Thank you for pointing out all of these missing details!

4 Likes

Remember that, while var wrappedValue is a requirement for property wrappers, init(wrappedValue:) is not. Initialization from a projected value would be very useful for out-of-line initialization of property wrappers that don't support init(wrappedValue:), but have a projected value that can be used for initialization.

Probably with an error message. I don't think this will be a big issue because in practice, most projection types include the wrapped-value type in some way.

I chose $_ to be consistent with the Swift grammar today. $ alone is not an identifier, but $_ is. This is also consistent with the way you call or reference a function that omits argument labels while writing those labels explicitly, for which you use _:.

5 Likes

I think that the real goal here is to achieve deferred initialization form a storage instance, not from an eventually available projected value. Projected values, technically, are in a surjective relation with their wrapped variables. A boolean projected value that returns true if its wrapped variable satisfies a given condition, isn't suitable to initialize that variable. The source of truth is the storage instance.

Let us consider State and Binding:

@State var x: Int
@State var y: Int
@State var z: Int

[$x, $y, $z].forEach { $element in
	type(of: element)  // returns 'Int'
	type(of: $element) // returns 'Binding<Int>'
}

Are we passing a State 's projected value or a Binding 's storage? The element's storage type is Binding, not State. Here, we are initializing a Binding from a storage instance. We are not initializing a State from its projected value.

2 Likes

I think it's best to add this [pass-by-projected value in memberwise-initializer] together with [update memberwise-init], whether now or later. Otherwise, having both old and new synthesis would be quite fractured and confusing.


What if we use static methods for projected value Wrapper.instance(fromProjectedValue:)? There's a good chance that we'd want to retrieve an already-existing instance. Especially that self-projection, like Binding, can simply use self.


I'm still ambivalent about projection vs storage. On the one hand, directly passing storage is a sure way to specify the same instance used. OTOH, we don't want to pass around storage for many types. Passing around things like State, Published, Environment, Lowercased is ill-advised at best. Somethings that we want to pass around are already mostly self-project, like Binding, UnsafePointer.

It also raises the question, if the projection, which is surjective to the storage (not wrapped value), is insufficient to identify the storage, is it appropriate for the storage to be passed around? If we have a Wrapper with Value wrapped value, and Bool projected value, then the only way to access the storage is via _value, which is already private.

1 Like

I also thought this was the goal all the way through the first review. However, a big point of criticism from reviewers was that the backing storage type should be private. The core team also thought that the backing storage type should be an artifact of the function implementation, and not exposed to function callers through the type system.

After the review, I thought long and hard about all of the use cases, and I realized that those people were right, because all of the use cases I had presented were actually passing a projection and not the storage type. Keeping the storage type private is consistent with how property wrappers work today. Unless a property wrapper projects its storage type via projectedValue, the storage type itself is meant to be private, implementation detail that cannot be accessed by API clients.

I agree, and in this case I would advise that such a property wrapper not implement init(projectedValue:). Similarly, not all property wrappers can be initialized from their wrapped value, in which case they don't implement init(wrappedValue:).

I actually think State and Binding are good examples of why we don't want passing the backing storage to be a goal. The State type is intended to be private, implementation detail, and you should never pass an instance of State to another view. Instead, you pass it's projection, which is Binding. Passing around Binding is totally fine because binding publicly projects itself via projectedValue. The public part of the API is projectedValue, not the storage type, and that's deliberate.

This example is initializing a Binding from its projected value, which is also Binding.

5 Likes

From the proposal:

A projected value can provide a setter different from what you get using init(projectedValue:). If a user adds an initial value, e.g. @Traceable var dataSource = "none", then in the initializer $dataSource = history would use the projected value's setter instead of init(projectedValue:), resulting in potential differing behaviors. Is this acceptable?


I was going to write it too. If the API provider's goal is to let users to access the storage type or bits of it, the only way is to expose them via the projected value. I agree with you and I'm now mostly fine with the proposal, thank you!

There's still the case in which the user happens to already have storage instances and its goal is to rely on the property wrapper just to "unwrap" the wrapped value and the projected value. In that case we can just force the user to define a wrapper variable in the function/for/case/while body and use the storage instance to explicitly initialize it. After all it's the same we do when we need to have a variable parameter inside a function body: we just redeclare it to be a var.

@propertyWrapper struct Wrapper<Wrapped> { ... }

for storage in storageInstances {
  @Wrapper var element: Wrapped
  _element = storage
  ...
}
2 Likes

This is already the case for init(wrappedValue:), isn't it?

1 Like

Yes, that's how DI works for initialization from a wrapped value. An assignment to a property with an attached property wrapper will get re-written to init(wrappedValue:) before all of self is initialized. Otherwise, assignment is re-written to the wrappedValue setter. The same rules would apply for init(projectedValue:) if we are to support it via DI.

I don't see your point here; could you elaborate?

Should there be a warning, though? Adding statements in the bodies of unusable functions results in a warning ("Will never be executed"). Likewise, there could be a similar warning –– and overall behavior, by allowing the function not to return a value –– for functions with wrapped parameters whose wrapper types lack any of the appropriate initializers –– init(wrappedValue:), init(projectedValue:).

Does the proposal support or extend casting syntax for disambiguation?

If so, then the above example I made would actually have a workaround solution:

let value = 42
foo($bar: value as W<String, Int>, b: Int.self)

// or do it manually
foo($bar: W<String, Int>(projectedValue: value), b: Int.self)

The value that automatically gets wrapped would require an explicit wrapper type for disambiguation.

That would not be possible. The type of $bar is B. The storage type isn't involved at all in foo($bar:). In order to solve that disambiguation I think we need to be able to explicitly set foo's generic parameter types on the call site:

foo<String, Int>($bar: 42, b: Int.self)
2 Likes

Well I disagree. We're talking about compiler magic and code synthesis here.

The proposal has this example:

let history: History<Int> = ... 
log(value: 10) 
log($value: history)

The compiler will inject a call to the appropriate property-wrapper initializer into each call to log based on the argument label, so the above code is transformed to:

log(value: Traceable(wrappedValue: 10)) 
log(value: Traceable(projectedValue: history))

Therefore log($value: history) doesn't really exist, but is reinterpreted as log(value: Traceable(projectedValue: history)).

That's why in my opinion log($value: history as Traceable<Int>) should probably be valid.

If I'm not mistaken the proposal doesn't state that the compiler will generate an overload func log($value:) which would call into func log(value:).

The explicit cast will tell the compiler how to properly interpret the wrapper type from the value to wrap (do not mix with the actual wrappedValue here).

let history: History<Int> = ... 
log(value: 10) 
log(value: 10 as Traceable<Int>) // same as above
log($value: history)
log($value: history as Traceable<Int>) // same as above

Furthermore, what if we had another func log(@MyWrapper value: ...) which would create a collision?

We either say that the above would be possible, or if not we would need to always write the boilerplate code manually again.

1 Like