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

I agree. It would also be coherent with the rules already in place for wrapped properties: if a protocol requires a property var foo: T, then any wrapped property @Wrapper var foo: T satisfies that requirement (it's the generated computed property var foo: T { get set } that satisfies the requirement).

Not quite, but it still applies in this case.

It's not the interaction model being identical but the type signature being identical. A function func foo(x: Int = 0) can be called as foo() but wouldn't satisfy a protocol requirement since its type is (Int) -> Void instead of () -> Void. The same applies for @autoclosure parameters: they can be instantiated as values, but they do not satisfy protocol requirements since func foo(x: Int) has a different type signature than func foo(x: @autoclosure () -> Int).
In this case both func foo(x: Int) and func foo(@Wrapper x: Int) share the same type signature.

That being said, since the proposed rules are more restricting, it can be seen as a possible future direction. Until then (if the direction appears to be viable), users can use local wrapped variables as suggested.

While I agree that the situation with protocol isn't ideal. I think improving it takes a lot more design consideration than only generating thunk. Especially that the init overload resolution is performed at the call site. As is, it seems to neither discounts any direction nor diminishes the current proposal's utility. Unless, of course, someone comes up with a better and more holistic story.

First of all: I really like the overall proposal.

There is on thing that I find suboptimal: While the proposed solution for closures may be helpful in some situation, I think there is a much more ergonomic solution. Let me explain:

The proposal suggests that an API author would create a function that takes a closure like this:

func useValue(_ handler: (Binding<String>) -> Void) { ... }

Then let‘s suppose there are two users of this API. User A wants to access the value as well as the binding. It would look like this:

useValue { (@Binding value) in
    print(value)
    doSomething(binding: $value)
}

One thing bothers me in this example: Every time, user A wants to use useValue(), she has to include @Binding.

Then let‘s turn to user B. He just wants to use the plain value. However, he does not know that property wrappers can be used in closures. So he does this:

useValue { binding in
    handleValue(binding.wrappedValue)
}

This does not look good to me. And even if user B would know property wrappers, he would have to include the @Binding:

useValue { (@Binding value) in
    handleValue(value)
}

My solution to this problem would be the following:

The API could be written like this:

func useValue(_ handler: (@Binding String) -> Void) {
    let binding: Binding<String> = getBinding()
    handler(binding)
}

User A could then write this:

useValue { value in
    print(value)
    doSomething(binding: $value)
}

No @Binding is needed.

User B, who doesn‘t know of property wrappers would look at the API, probably google @Binding and find out about the concept. Then, he could just write:

useValue { value in
    handleValue(value)
}

Or even simpler:

useValue(handleValue)

Here is another example:

let songTitles: [String] = ...
songTitles.forEach { title in
    print("\($title.index): \(title)")
}

This would eliminate the need to use enumerated().

And one last example:

ForEach($entries) { entry in
    HStack {
        Text("\(entry):")
        TextField("Entry", text: $entry)
    }
}

What do you think?

Thank you for the review!

This suggestion has come up a few times (and I should probably add this to Alternatives considered). I personally am against allowing property wrapper custom attributes as type attributes, because I think it would confuse the property-wrapper declaration model. For example, if you can write @Binding String, then why can't you write var value: @Binding String? Or would that work under this model and now there are two ways to declare property wrappers?

I also think it's useful to have some indication (via syntax) at the parameter declaration that you have a property wrapper. I agree that sometimes the wrapper attribute doesn't need to be written out explicitly. There's a special bit of sugar in the proposal for the case where a property wrapper's projected value is the same type as the wrapper itself:

For closures that take in a projected value, the property-wrapper attribute is not necessary if the backing property wrapper and the projected value have the same type, such as the Binding property wrapper from SwiftUI. If Binding implemented init(projectedValue:) , it could be used as a property-wrapper attribute on closure parameters without explicitly writing the attribute

So, if Binding were to implement init(projectedValue:) your last example could look like this:

ForEach($entries) { $entry in
    HStack {
        Text("\(entry):")
        TextField("Entry", text: $entry)
    }
}

I like this because you don't have to write @Binding, but you still have an indication that you're working with a property wrapper in the closure body.

3 Likes

Thank you for the detailed response. Could this particular concern be alleviated by writing it as follows?

(@Binding _: String) -> Void

or

(@Binding _ value: String) -> Void

This wasn‘t clear to me. Thank you for mentioning it. I think this is better than having to write @Binding every time. However, I think, the solution I mentioned would create many opportunities to improve existing functions (such as my forEach() example) by retaining the function signature (as seen by the user) but allowing users to access additional information (such as an index).

1 Like

I think what you're suggesting is allowing property wrapper attributes only in parameter type positions to force the argument function to have the same wrapper attribute on that parameter, and to allow inference of the wrapper attribute on closure parameters. I still think a syntactic indication that you have a property wrapper is valuable, but I also don't think there's anything in this proposal preventing such a feature from being proposed in the future.

FWIW, there are a few more downsides that I outlined in the first review:

That sounds correct. However, I fear that by the time such a feature will be added, many APIs such as

func doSomething(handler: (Binding<String>) -> Void)

will have been added. It would then be a breaking change to change it to:

func doSomething(handler: (@Binding _: String) -> Void)

I maintain that expanding property wrapper attributes to types, even if only in the parameter position, is out of scope for this proposal. What you're proposing is a bit of sugar on top of this proposal that comes with a lot of implementation complexity and downsides that haven't been fully considered yet. It also doesn't change anything about the base model of property wrappers on parameter declarations, so nothing is preventing someone from proposing this in the future.

The APIs you mention already exist today, because the only way to pass property wrappers around is to use their type directly. There are deprecation tools for APIs if a better solution comes along.

6 Likes

I can relate to your concerns.

1 Like

The four major changes in this version of the proposal together address essentially all of the major concerns voiced during the prior review, and they improve this proposal for the better. Overall, I think the design outlined here is about as intuitive as such a complex feature could get. It certainly now fits much better with the feel and direction of Swift.

I haven't thought up many use cases for it myself, but I can see the benefit for certain use cases justifying the addition of this feature; I haven't used other languages with a similar feature but I have certainly tried to pay close attention to this proposal from its earliest stages, and I put in a detailed study of this iteration of the proposal as well as all preceding feedback.

Again, fantastically well done and what a wonderful example of incorporating community feedback to strengthen a very useful and well thought-out proposal.

9 Likes

Would that proposal allow to use property wrappers in the constructors?

class Foo {
   let bar: Bar

   init(@Injected bar: Bar) {
      ...
   }
}

I'm thinking of making dependency injection solution using this feature.

Yes, the proposal enables applying property wrapper on parameters of functions –– aka global functions, initializers, instance methods, static functions, and subscripts (plus any function declarations I may have forgotten):

struct App {

  // Initializer
  init(@Asserted(.greaterOrEqual(1)) screens: Int) { ... }

  // Subscript
  subscript(@Asserted(.greaterOrEqual(1)) screen: Int) { ... }

}


struct LoginController {

  // Static function 
  static func checkAvailability(
    @Lowercased @Asserted(!.isEmpty) of username: String
  ) { ... }
 
}


// Global function
func loginController(@Clamped(0 ... 10) for index: Int) { ... }
2 Likes

I was made aware of this review in this post.

I love everything I see, except I am not comfortable lying or asking people to lie. We have to either

  1. Rename the feature to variable wrappers or similar.

or

  1. Redefine property to mean any variable or constant in any scope.

I don't think it's lying. @John_McCall points out in the thread you linked to that the property wrappers feature was "named after its most important use-case". Furthermore, as @hborla mentioned in that thread, property observers and computed properties also have "property" in their names besides not being exclusively meant to be used for properties.

I think a change in the documentation –– or even the compiler messages –– would be reasonable if it is agreed that action needs to be taken to address the property wrappers' name. Nevertheless, the proposed feature is not directly related to naming. Thus, if you think a change is required, it should probably be in a different proposal altogether.

3 Likes

I don't think the dishonesty is backed by malicious intent. I just think it doesn't matter to some of the people involved, enough to justify any work it would take to be correct. I'm not comfortable with that, but I can understand someone trying to rationalize around the falsity.

Too much of a time investment; I don't think it would be worthwhile. :slightly_frowning_face: There's two of you very intelligent people already who didn't read the little bit of documentation-quoting I did already on computed variables.

Please don't make assumptions like this about other participants on these forums. I did read your documentation quote. My point was that the feature is generally referred to with the term "property", as are other similar features that apply to all forms of named data, and the terminology is not new in Swift 5.4 or with this proposal. This phenomenon even exists in other languages. You can drop the term "property" or substitute it with "variable" when the feature is used in a different context, just like the documentation does.

Let's take any further discussion on this topic back to the other thread.

7 Likes

I am +1 for this proposal.

I think the rationale for having the wrapping done by the caller is well laid out and is the correct choice. The proposal also does not preclude a callee from accepting a passed-in value and wrapping it within the called method, so this proposal also allows for both approaches.

The concrete example in my own coding experience related to this proposal is the case of propagating bindings through a ForEach in SwiftUI. I believe the proposal addresses that case very well and will lead to more expressive code.

Considering that case as well as the other examples in the proposal, I think the problem addressed is signifiant enough to warrant the addition. I also believe the proposal does fit well with the feel and direction of Swift.

This is an intricate feature, but I think the proposal adds it to the language in a straightforward and consistent manner.

I think it will take a considerable amount of knowledge to use this feature well in libraries and frameworks, understanding property wrappers themselves and then the implications of using a property wrapper for a parameter. I think the cognitive load will be much less for clients of those APIs, where specific property wrappers are used and idiomatic use will likely be clear from example code or documentation, even without a deep knowledge of property wrappers or this feature.

I read the proposal and feedback for this review closely, but did not follow the previous review closely.

This proposal also seems to be a great example of the Swift Evolution process at work. I really appreciate the work done by the proposal authors and everyone who contributed to the reviews.

1 Like

+1 on this revision. Pre-concurrency use cases may not fully justify the effort, but this is changing. Now with actor concurrency on the horizon and its parameter requirements, it is going to be a very useful feature. Especially for library authors and API designers, particularly for transitory and adapter APIs.

How so? So far no additional property wrappers have been introduced as part of the concurrency effort, so where do you see this proposal helping in that regard?

Mostly just have a hunch about this. I was working on migrating some of my code and it seems l am going to be able to put this feature to good use. I am guessing Apple might use it in some future API, especially to ease transition of legacy code.