SE-0293 (third review): Extend Property Wrappers to Function and Closure Parameters

Hello Swift community,

The review of "SE-0293: Extend Property Wrappers to Function and Closure Parameters " begins now and runs through March 29, 2021. The proposal is available here .

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager. When emailing the review manager directly, please keep the proposal link at the top of the message.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available here .

Thank you,

Chris Lattner
Review Manager

13 Likes

This is fantastic. The revision not only addresses prior concerns in an elegant way, the text is extremely well written. This proposals adds a powerful new capability to property wrappers in an elegant way that fits with the existing feel and direction of Swift. I haven't used other languages with similar features (does Kotlin have something like this?). I've followed all prior iterations and read this proposal again carefully.


One specific point of clarification. In the text it says, under "Detailed design":

API property wrapper attributes can only be applied to parameters in overridden functions or protocol witnesses if the superclass function or protocol requirement, respectively, has the same property wrapper attributes [emphasis added].

Rationale : This restriction ensures that the call-site transformation is always the same for families of dynamically dispatched functions.

It later says, under "Future directions":

API property wrappers in protocol requirements

Protocol requirements that include property wrappers was pitcheda while ago, but there was a lot of disagreement about whether property wrappers are implementation detail or API. With this distinction formalized, we could allow only API-level property wrappers in protocol requirements.

So...is the use of property wrappers in protocol requirements part of the detailed design of this proposal, or a future direction that isn't part of this proposal? (I would very much love for it to be the former, but totally understand that the work has to stop somewhere if it's to ship.)

5 Likes

Sorry, it's still a future direction :slightly_smiling_face: the restriction was intended to specify how it should work if/when property wrappers are allowed in protocol requirements (but I understand how the way it's written is a little confusing). Given that property wrappers are not supported in protocol requirements today, the implication is that API property wrappers cannot be used on parameters of a protocol witness.

+1. Excellent.

As I said in the previous pitch thread, this iteration of the proposal feels like an excellent balancing act between many competing constraints, and I'm really pleased with where it ended up. No notes!

+1

Looks great, exciting stuff! Got a question though;

From the proposal: "Currently, applying a property wrapper is solely permitted on local variables and type properties"

Is it available for local variables? When I do...

func testing() {
    @Published
    var string: String = "test"
}

I get a Property wrappers are not yet supported on local properties. Running Swift 5.3.2.

That’s because it’s supported in Swift 5.4 :)

5 Likes

I read through but probably overlooked this in the proposal, why does an implementation detail wrapper go in the function definition? Even if it's erased outside the module boundary, within the module it's still visible then to callers. This goes counter to general programming practices, don't put implementation details in the API. I suppose it's for convenience of not having to manually create a wrapper internally in the function, but there's probably a convenience around that to create instead which also simplifies the mental model.

Parameters of a function both describe the interface of a function and declare a local variable within the function. Parameters already contain information that is not used outside the function implementation - the internal parameter name. For both API and implementation-detail property wrappers, the property wrapper describes the semantics of the local variable. The only difference is that API property wrappers allow the caller to pass a different type of argument.

Yes, it's for convenience. Of course, an alternative is to force the programmer to write a local property wrapper, but that does impose extra boilerplate.

2 Likes

I am +1 for this proposal.
I followed it on and off through the pitch and review phases.

I do find this to be an intricate and complex advanced feature, but in use, I think most of the complexity falls to the API producer, while the caller can typically make calls without deep knowledge of the details. As in using property wrappers, the core concept of wrapped value versus projected value, and then the syntax to reference the correct one, are the main things a developer needs to wrap their head around when working with this feature.

I had one point I wanted to clarify my understanding about:

In the section on closures, a projected value can be passed in. I am inferring from how these are handled in functions that within the closure the projected value is accessed using the $value identifier and the wrapped value can be accessed using the value identifier.

Is my understanding correct? I seem to remember that being explicitly noted in one of the previous versions of the proposal.

1 Like

Yes, you are correct.

The dollar sign in the $value identifier signifies the initialization method — which, in this case, is via the projected-value type. As for accessing the wrapped and projected values, value are $value are used respectively — following existing precedent.

The actual transformation is this:

let useBinding: (Binding<Int>) -> Void = { $value in
  ...
}

// ---------------------- Becomes  ⬇️ ---------------------- \\

let useBinding: (Binding<Int>) -> Void = { (_value: Binding <Int>) in
  var value: Int {
    get { _value.wrappedValue }
    set { _value.wrappedValue = newValue } 
  }

  var $value: Binding<Int> {
    _value.projectedValue
  }

  ...
}
3 Likes

A property wrapper will only be inferred as API if init(projectedValue:) is declared directly in the nominal property wrapper type.

I believe we never actually define "nominal type", even with the current language definitions. IIRC @hborla infer in one of the comments that it's the original definition of the type (excluding extensions, even in the same module).

Maybe we want to instead use something like "original definition (excluding extensions)"?


Ok, so we can't convert Impl to API by introducing the init(projectedValue:) outside of the nominal type. Instead, can we override the chosen init(projectedValue:)/init(wrappedValue)?

@propertyWrapper
struct Wrapper<Value> {
  init(projectedValue: Value)
  init(wrappedValue: Value)
}

extension Wrapper where Value == Int {
  init(projectedValue: Value)
  init(wrappedValue: Value)
}

func foo(@Wrapper x: Int)

Which ones would foo use in this case? The only example in overload resolution of backing property wrapper initializer have the init be in the nominal declaration, so it's somewhat ambiguous here.


How does the composition work when I mix the API and Impl wrappers?

func foo(@API @Impl x: Value)
func bar(@Impl @API x: Value)

foo shouldn't be able to hide @Impl since it's tied to the type of the projected value, we should still be able to hide it if API only defines init(wrappedValue:).

bar may be able to expose itself as func bar(@API x: Value) using a similar reasoning.

Nonetheless, this sounds like a pretty complex composition rule.


Some rambling about Impl vs local var wrappers

I've been figuring out for the longest time the proper distinction between the Impl and local wrappers. It bothers me because similar features with subtle distinction are dangerous for the language as a whole. Said distinction confuses those that are new to both features and invites debates about best practices surrounding it. This has much less to do with the scope of the proposal. I'm not implementing it :smirk:, so I couldn't care less if the proposal becomes too big, so long as it doesn't grow to a manifesto size.

If we look at the mutability, Impl is a poor feature since it only locks down the mutability. Property wrappers ought to be able to do it anyway (we'll need a separate proposal). Furthermore, if our recommendation is to "use Impl when you don't mutate, and use local var when you mutate", it goes against the Swift philosophy that mutability is easy to enable/disable, generally by swapping let/var, and that invites creating local functions just to create Impl wrapper.

However, now that I start to internalize @John_McCall's interpretation (for lack of a better word):

I start to think that maybe the actual recommendation would be to "always use Impl when you wrapper argument and keep on using local wrapper elsewhere". This... sounds obvious in hindsight, but I can say that it's not easy to reach this conclusion. This makes me believe that there is just enough distinction for both Impl and local wrappers to live comfortably in the same language.

2 Likes

It might be a little weird to leave a review on your own proposal, but I have a small suggested modification. From the proposal:

closures parameters do not distinguish between implementation-detail and API property wrappers; all property wrappers will be initialized from the appropriate argument in the order they appear in the parameter list before the closure body is executed.

This was an "ease of implementation" artifact, and I feel that it's important for evaluation order of property wrapper initializers to be the same regardless of whether the property wrapper attribute is declared on a closure parameter or a regular function parameter. I think the proposal should be modified so that closure parameters follow the same rules for API versus implementation-detail property wrappers, and $ closure parameters necessarily have to be API wrappers because the caller will always pass a projected value.

4 Likes

Sorry about that, "nominal type" is definitely compiler jargon. I will clarify this wording in the proposal. Thank you!

Yes. Once it has been determined that the property wrapper does support initialization from a wrapped or projected value, the initializer expression is built and type checked normally (at the function declaration site, not the call-site of the function), so initializers in extensions can be chosen. Your example should choose the init(wrappedValue:) and the init(projectedValue:) in the extension.

It's a lot more straightforward than other composition rules. Since projected values aren't composed -- they always represent the projected-value of the outermost property wrapper -- the "does this wrapper support projected-value initialization?" decision is based on the outermost property wrapper. So, in this example:

You're right that foo does not hide @Impl unless @API only has init(wrappedValue:). The second function bar does not expose any of those wrappers because a property wrapper of type Impl<API<Value>> cannot be initialized from a projected-value, assuming that Impl does not have an init(projectedValue:).

2 Likes

It is expectable, though, that you can pass a projected value of API with a synthesized bar looking something like:

func bar(x _x: API<Value>) {
  let _x: Impl<API<Value>> = Impl(wrappedValue: _x)

  var x: API<Value> { get set }
  var $x: API<Value>.Projected { get set }
}

// call site
bar($x: projected)
// synthesized call site
bar(x: API(projectedValue: projected))

Now, it is a complex rule, but it is also oddly intuitive that I think is worth pointing out.

1 Like

Hmmm, I disagree that this is expected behavior (and in case it isn't clear, this is not something supported by the proposal). When you pass a wrapped value, you only pass the wrapped value of the innermost property wrapper - you can't intercept the initialization somewhere in the middle of the composition chain. With projected value, you can only pass the projected value of the outermost property wrapper. With this proposal, there is also no way to mix wrapped-value and projected-value initialization in the way you're describing, although this could potentially be explored in the future if it's desirable somehow.

2 Likes

I came from a different angle. Since API is meant to be exposed and Impl is meant to be hidden. We're missing a scenario where one exposes an API and hides an Impl on the same variable. Compositing API and Impl together is just something I tried to reach first. It could also be that we need an entirely different annotation altogether.

TBH, I don't think this scenario is very common. To begin with, wrapper composition is rare. More so for a mixed Impl API.

1 Like
  • What is your evaluation of the proposal?

+1. I look forward to seeing what can be built with this feature.

  • Is the problem being addressed significant enough to warrant a change to Swift?

Yes. Lifting limitations on property wrappers to provide more flexibility without overly complicating the feature is a great goal.

  • Does this proposal fit well with the feel and direction of Swift?

Yes. Property wrappers as custom attributes have become more and more popular within the community and this expansion of their capabilities will push a popular feature even further.

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

N/A

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

I observed the previous discussions at a distance, so I've done a quick reading of this final proposal.

FWIW, in general usage, a "nominal type" is a type with unique identity beyond just the structure of its components, whereas the identity of a "structural type" is only based on that internal structure. It's called "nominal" because in the simplest, most common language pattern — something like a named struct at global scope — you can think of the type's identity as just being its name. In reality, the identity is usually more complex than that because it incorporates context; e.g. in C you can write struct { ... } in arbitrary places, and every struct keyword used that way creates a new type.

More formally, you can think of every type in the language as a type constant applied to some number of argument types. For example, Int is the type constant Swift.Int applied to no argument types, and (Int, Float) is the type constant <tuple> applied to the argument types Int and Float. In this view, all of the type constants are "nominal", and the interesting thing about the class, struct, etc. features is that they allow you to introduce new type constants, whereas <tuple>, <function>, <metatype>, and so on are built-in to the compiler. (For simplicity, ignore the additional structure that some of these built-in types can carry, like conventions, element labels, etc.)

Holy was using "nominal type" in a different, more jargon-y sense: in the compiler, the primary declaration of a nominal type is represented with a class called NominalTypeDecl, and so she was talking about looking something up in just that declaration without considering extensions. The more standard user-facing term for this is, well, "primary declaration".

15 Likes