Pitch #2: Extend Property Wrappers to Function and Closure Parameters

I think most of the Future Directions section could be moved into a new manifesto document.

I think this is a good idea! There are a lot of ideas and discussions floating around about future directions for property wrappers in general. It would be nice to have them all documented in one place and easily accessible.

1 Like

This is exactly what I was going to post.

If an attribute applies to a specific expression (whether it's a closure, or something else), I'd expect it to be represented in the syntax tree by a new AttributedExpr node that would contain the AttributeList and the Expr being wrapped. So this would be:

AttributedExpr(
  attributes = [Attribute(@convention(c))],
  expr = ClosureExpr(...)
)

This syntactic representation generalizes easily to any expression (i.e., someone could write 5 + @foo 10 * @bar (x - y) if there was ever a context where that made sense one day), unlike the nested form where the attribute list is something hardcoded just for closures.

It would also fix SR-13711 by letting attributes apply to ArrowExprs which are used to represent function types parsed in expression contexts, which is currently tripping up swift-format and is why I was thinking about this in the first place. :smile:

4 Likes

I just posted in the Result Builders' thread if it would be possible to place annotations between argument labels and parameter names:

The example shown in the proposal would then be:

func reportProgress(
  of @Percentage progress: Int
) { ... }

which, although being a minor edit, may read better.

1 Like

Looks brilliant, nice work folks

3 Likes

... not all "guys".

We strive to build an inclusive Swift community here, which means trying to avoid exclusionary terminology. While "you guys" may have historically been considered gender-neutral, it is not considered to be not gender-inclusive now and we should avoid it. There are numerous other ways to refer to more than one person without resorting to "guys".

[EDIT: Provided more context for my correction.]
[EDIT #2: Edit missed a ']'.]

Doug

26 Likes

I can't think of any particular ambiguity. It is a little bit interesting for trailing closures, because the { would no longer come immediately after the previous argument. I don't think it's a technical limitation---the parser can do arbitrarily lookahead past the attributes---but visually I think it's much harder to associate the trailing closure with the prior call if there were attributes between.

Doug

1 Like

I'm a bit confused by these 'attribute applied to the entire closure' examples. In this thread (unless I've overlooked something) we've discussed two kinds of attributes: 'type' attributes and 'declaration' attributes. I thought that @convention(c) was considered a type attribute, so the proper way to spell this would be:

let callback: @convention(c) () -> Void = { ... }

or

let callback = { ... } as @convention(c) () -> Void

Even broadening the discussion past the @convention(...) attribute, I'm not sure what the suggested placement of the 'entire closure' attribute is supposed to communicate. Whether the attribute is placed before or after the opening brace, it doesn't seem to fall into either of 'type' or 'declaration' buckets—it looks to me like a new kind of attribute, which labels a particular expression rather than a type or declaration.

Also, regarding trailing closure expressions, we can already write:

func foo(_: @convention(c) () -> Void) {}

foo {
    print("Passed as C function pointer")
}

What does the 'expression attribute' buy us?

2 Likes

+1 from me, it's a good idea.

While we're at it, here's another thing I would like to be able to do:

@propertyWrapper
struct Queued<A> {

let wrappedValue : (A) -> Void
    
    init(wrappedValue: @escaping (A) -> Void, queue: DispatchQueue) {
        self.wrappedValue = {a in queue.async{wrappedValue(a)}}
    }

}

struct Bar {

@Queued(queue: .main)
func foo(arg: Int){...} 
//Error: Property wrapper attribute
//'Queued' can only be applied to a property

@Queued(queue: .main)
var bar : (Int) -> Void = {print($0)} //works right now

}

4 Likes

Now that I think about it, in the absence of contextual information (outside function argument), they should look more like:

@declAttribute var foo: @typeAttribute = { ... }
2 Likes

One question that occurred to me: would this pitch allow for specifying a property wrapper on a function parameter in a protocol?

E.g.,

protocol Foo {
  func bar(@Wrapper arg: Int)
}

I assuming the answer is “no” since we don’t allow wrappers on protocol property requirements.

Also, with respect to the API resilience section, I was under the impression that that section was intended to answer questions like “is adding/removing a wrapper to/from a public function’s parameter an API breaking change?”—is that a misunderstanding on my part? If not, would the authors mind elaborating on that point a bit (or pointing out if I’ve missed something already mentioned in this thread)?

One question that occurred to me: would this pitch allow for specifying a property wrapper on a function parameter in a protocol?
I assuming the answer is “no” since we don’t allow wrappers on protocol property requirements.

Yes, your assumption is correct. I'll make this explicit in the proposal.

Also, with respect to the API resilience section, I was under the impression that that section was intended to answer questions like “is adding/removing a wrapper to/from a public function’s parameter an API breaking change?”

Yes, you're right about this, and adding/removing a wrapper to/from a public function parameter is not a resilient change. I changed this section in the PR but I forgot to update the text in this post- sorry about that! Here's what the section looks like:

Effect on API resilience

This proposal introduces the need for property wrapper custom attributes to become part of public API. A property wrapper applied to a function parameter changes the type of that parameter in the ABI, and it changes the way that function callers are compiled to pass an argument of that type. Thus, adding or removing a property wrapper on a public function parameter is an ABI-breaking change.

2 Likes

Ah, should have checked the linked document. Thank you for reiterating that!

Asked and answered up-thread!

Having thought on this a bit more, I'm curious if there's a compelling reason for this to be the case. The current proposal says that a function declared as:

func reportProgress(
  @Percentage of progress: Int
) { ... }

reportProgress(of: 50)

gets implicitly transformed to:

func reportProgress(of _progress: Percentage) {

  var progress: Int {
    get { _progress.wrappedValue }
  }
  ...
}

reportProgress(of: Percentage(wrappedValue: 50))

However, I don't right off the bat see why the caller-side transformation is necessary. I.e., why couldn't the transformation of the body of reportProgress take the form:

func reportProgress(of progress: Percentage) {
  let _progress = Progress(wrappedValue: progress)
  var progress: Int {
    get { _progress.wrappedValue }
  }
  ...
}

leaving the call reportProgress(of: 50) unchanged? AFAICT this would make the addition of a parameter wrapper ABI- (and API-) compatible, but I don't know if I'm overlooking potential wins from the current setup.

As a (potentially related) aside, would the injection of the wrapper type into the calling context have the ability to affect the inferred type of the calling expression? I can imagine a somewhat pathological example, again using the Percentage example from the post:

@propertyWrapper
struct Percentage {
  init(wrappedValue: Double) { ... } // Note: not `Int`!

  var wrappedValue: Int {
    get { ... }
    set { ... }
  }
}

func reportProgress(
  @Percentage of progress: Int
) { ... }

reportProgress(of: 50)

Again, under the proposed rules, the call to reportProgress becomes:

reportProgress(of: Percentage(wrappedValue: 50))

If the pre-transformation code compiles (does it?), a literal which by all appearances has type Int (an integer literal passed to an argument of type Int) is instead inferred to have type Double!

Pure pedantry

If the proposed semantics of this transformation as written are desired, would it be more apt to refer to this feature extension as "function argument wrappers" rather than "function parameter wrappers"?

1 Like

Your question was answered here:

But perhaps it could be added to the Alternatives Considered section.

2 Likes

Oop, thanks Ben. Checked alternatives considered but didn’t check through the thread this time :man_facepalming:. Still curious whether it’s desirable for the wrappedValue overloads to influence the type of the passed argument in potentially non-obvious ways.

Still curious whether it’s desirable for the wrappedValue overloads to influence the type of the passed argument in potentially non-obvious ways.

Overload resolution can't influence the type of the wrappedValue argument in an unexpected way, because it's an error to have a wrappedValue parameter type that isn't compatible with the type of var wrappedValue in the property wrapper. autoclosures of compatible types are allowed, though.

I don't think the need for this overload resolution behavior will be super common, but I do think it's reasonable to want. Personally, I think the most compelling argument for applying the property wrapper to the argument at the call-site is the ability to initialize the wrapper differently, e.g. through a projected value or by passing the backing wrapper directly.

If the proposed semantics of this transformation as written are desired, would it be more apt to refer to this feature extension as "function argument wrappers" rather than "function parameter wrappers"?

FWIW, here's the terminology I've been using: You attach a property wrapper custom attribute to a parameter declaration, and the property wrapper is applied to the argument at the call-site.

1 Like

But perhaps it could be added to the Alternatives Considered section.

Yes, I should (and will) add all of this to Alternatives Considered, thank you!

2 Likes

Ah, gotcha! That makes sense. In that case, if the argument expression is always supplied to a wrappedValue parameter with the same type as the un-transformed argument, I'm having a bit of trouble picturing what that this overload resolution behavior looks like. Whenever you add the item to Alternatives Considered, would you mind including an example to illustrate this behavior?

Yeah, I agree that's useful. Of course, if the author of a function with a wrapped parameter wants to provide that behavior, isn't it easy for them to supply a func reportProgress(of percentage: Percentage) overload? I'm slightly wary of whether it's worth forcing authors to introduce wrappers into their library's ABI when they could offer convenience overloads themselves, or, potentially, auto-generate such overloads with some future extension of this feature (such as, say, func reportProgress(public @Percentage of progress: Int)).

Initializing the wrapper differently is also necessary for the closure parameter design in this proposal. If (@Wrapper value: Int) -> Void means the function type is (Int) -> Void, then property wrapper parameters cannot be utilized to opt into property wrapper syntax for closures that take in wrapper types. The best you'd be able to do for the Binding example in the proposal/up-thread is declare a local property wrapper and initialize it out-of-line using the backing storage directly:

ForEach($shoppingItems) { binding in
  @Binding item: Item
  _item = binding
   
  TextField(item.name, $item.name)
}

I realized that this might not be obvious, but not all property wrappers support init(wrappedValue:). Changing the transformation to be callee-side would mean that this feature can never be used for property wrappers that need to be initialized differently.

1 Like

This example also caused me lots of confusion. In the next revision, it should be replaced by something realistic that hangs together logically (and if you can't find such a case, that should tell you something!).

Another point: since it complicates the declaration, IMO exposing this sort of parameter adjustment to users in APIs may only be valuable to the extent that it allows the API's doc comment summary to be written more simply, because the adjustments are implicit in the property wrapper. (e.g. in terms of the API on the wrapper). So when looking at these examples, IMO we should examine what happens to the doc comment summary.

Edit On further reflection, In the vast majority of cases, this sort of argument normalization is an anti-pattern in an API, because it turns detectable bugs in the calling code into silent misbehavior. In general, it is much better to put preconditions on parameters, which offer the API author a choice about how/if to respond to violations. I have good reasons to want more flexibility from property wrappers, but IMO this case is a poor motivator.

1 Like