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

This is a typo right? I think you mean offset , not percentage ? Wouldn't the latter be clamped automatically? Btw in this case it doesn't seem to make sense to clamp offset, since the result will be clamped anyway, but that's a detail.

Yes, it's a typo - sorry for the confusion! In this case, clamping the offset does change the behavior if the offset is negative. It's a contrived example, but it's meant to remind the reader of assertions or adjustments they write on parameter values that would be useful to 1) write an abstraction for, and 2) be able to communicate to callers through the API itself.

And a question: how would this work in practice? It seems to me that this can't be achieved without adding an overload to ForEach ?

Yes, the proposal does call out that this specific example would require an appropriate ForEach initializer. It's meant to be an example of what library authors could create using this proposed feature. That said, this example would work with the existing ForEach initializer if you extend Binding to conform to RandomAccessCollection (where the binding value is a collection), so if you wanted to try out this example with the PR toolchain once it's ready, you can!

Maybe we can have parentheses-less case be application to argument, and require parentheses when applied to the whole closure:

{ @Foo a in /* wrap a */ }
{ @Foo (a) in /* wrap closure */ }

IMO, initialization from projected value should rather be passing the wrapper directly.

Haha that's exactly what I just tried, and it does work with Swift 5.3. Sorry I missed that you mentioned the new initialiser, I was confused by what the exact purpose was, it seemed to me that the annotation on the closure argument (@Binding shoppingItem) didn't add anything since the shoppingItem would already be a binding automatically.

In the clamp example I see the idea, you apply a transformation to the argument, that could be useful. But with bindings I'm confused, because if the shoppingItem in the closure isn't already a binding, it seems that we could never turn it into one on the fly in a meaningful way, I mean what would it then bind to?

extension Binding: RandomAccessCollection where Value: RandomAccessCollection { }

struct Item: Identifiable {
    var id: String
    var name: String
}

extension Binding: Identifiable where Value: Identifiable {
    public var id: Value.ID { wrappedValue.id }
}

struct MyView: View {
    @State private var items: [Item]

    var body: some View {
        ForEach($items) { item in // item is already Binding<Item>
            TextField("Enter the item's name...", text: item.name)
        }
    }
}

And of course you are right about clamping negative offsets.

I like this design~

Can the transformation be restricted to the function body, so that adding/removing @Percentage doesn't break ABI?

// Original
func reportProgress(
  @Percentage of progress: Int
) {
  if progress.isMultiple(of: 10) {
    print(progress)
  }
}
// Transformed
func reportProgress(
  of progress: Int
) {
  // Synthesized by the compiler.
#if __inout__(progress)
  var _progress = Percentage(wrappedValue: progress)
  defer { progress = _progress.wrappedValue }
  var progress: Int {
    get { _progress.wrappedValue }
    set { _progress.wrappedValue = newValue }
  }
#else
  let _progress = Percentage(wrappedValue: progress)
  var progress: Int { _progress.wrappedValue }
#endif
  do {
    // Unchanged from the original.
    if progress.isMultiple(of: 10) {
      print(progress)
    }
  }
}

The synthesized do statement exists so that progress can still be shadowed (SE-0003).

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

1 Like

Can the transformation be restricted to the function body, so that adding/removing @Percentage doesn't break ABI?

I thought about this a little bit originally, but there are some issues with this approach:

  1. This limits the ability for overload resolution to choose init(wrappedValue:) based on the type of the argument passed, rather than the parameter type.
  2. This prevents us from being able to extend the feature to enable the call-site to pass a projected value or the backing wrapper directly.
1 Like

It's not "this one case", though. You have to parenthesize to add a type annotation:

_ = { i: Int in  // error: consecutive statements on a line must be separated by ';'
  print(i)
}

Dreadful error message, but the parentheses-less form is already very limited.

Calling conventions are one example that would fit into the language today, e.g.,

let callback = { @convention(c) result in print(result) }

Looking forward to concurrency, there might be some need for attributes on closures there that state something about where we expect the closure to be run (on a particular actor, concurrently with its context, etc.), so my concerns are somewhat speculative. I don't want to complicate the future for a local syntactic optimization here.

Doug

2 Likes

Best be if we just always require parentheses for closure with anything other than labels, which should be congruent w/ the current behaviour. We can just relax them later after we have more attributes. Though I feel like we could make it so that:

  • Require parentheses for cases w/ type annotation and/or type attributes.
  • Can omit parentheses for cases w/ only labels and/or declaration attributes.
  • Declaration attributes bind more strongly to the label than to the closure.
1 Like

I was confused by what the exact purpose was, it seemed to me that the annotation on the closure argument ( @Binding shoppingItem ) didn't add anything since the shoppingItem would already be a binding automatically.

Yes, the closure parameter is already a Binding. Using the @Binding attribute enables the property wrapper syntax in the closure body, so you can easily access the wrappedValue and the binding itself. Perhaps a more enlightening example of why you might want this is:

struct MyView: View {
    @State private var items: [Item]

    var body: some View {
        ForEach($items) { (@Binding item) in
            TextField(item.name, text: $item.name)
        }
    }
}

In your example, you'd have to write item.wrappedValue whenever you want to access the item itself.

IMO, initialization from projected value should rather be passing the wrapper directly .

Could you please elaborate on this suggestion? Are you suggesting using the syntax outlined in the future direction (e.g. fn($arg: backingWrapper)) but without wrapping the argument in a call to init(projectedValue:)? Or are you suggesting a different syntax for enabling passing the backing wrapper directly?

I personally think using the $ syntax for passing the backing wrapper directly would be confusing, because $ represents the projected value, which 1. not all property wrappers have, and 2. doesn't necessarily have the same type as the wrapper itself. I would think that if we support passing the backing wrapper directly, it'd naturally look something like fn(_arg: backingWrapper)

Neat. Is there any syntactic ambiguity that arises if these sit outside the braces? Seems apropos given our discussion on the other side about placing attributes ahead of the thing they’re labeling, and not after:

let callback = @convention(c) { ... }
// versus
let callback = { @convention(c) in ... }
4 Likes

I don't have a strong opinion on the syntax itself, but I think that it'd be more common to pass in the wrapper instead of the projected value. It is particularly important when the wrapper itself is class-bound.

1 Like

Doesn't this limitation also apply to memberwise initializers from the original SE-0258 proposal?

I'd be surprised if this was accepted. For comparison, we can't directly pass a closure argument to an @autoclosure parameter, and autoclosure forwarding is banned in Swift 5 mode (SR-5719).

If the reportProgress function is able to use @Clamped(to: 0...100) instead of @Percentage, the author wouldn't expect/want an argument with different limits, e.g. Clamped(wrappedValue: Int.min, to: Int.min ... Int.max)

Doesn't this limitation also apply to memberwise initializers from the original SE-0258 proposal?

Yes. As Doug alluded to earlier, this proposal can potentially improve the way memberwise initializers are generated when property wrappers are involved.

I'd be surprised if this was accepted. For comparison, we can't directly pass a closure argument to an @autoclosure parameter, and autoclosure forwarding is banned in Swift 5 mode (SR-5719).

I'm confused by the purpose of this comparison. What is this meant to illustrate?

If the reportProgress function is able to use @Clamped(to: 0...100) instead of @Percentage , the author wouldn't expect/want an argument with different limits, e.g. Clamped(wrappedValue: Int.min, to: Int.min ... Int.max)

I agree. In this proposal, property wrappers with additional arguments in the wrapper attribute are not supported on parameters (that's why we were trying to find a different example of a wrapper that doesn't need other arguments on the PR). There have been a few ideas for how to support this in the future while ensuring that callers can't change the arguments specified in the wrapper attribute.

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