Extend Property Wrappers to Function Arguments and Closure Parameters

Warning

This proposal assumes that this proposal has been added to the language, since this proposal is in the earliest stage of evolution and feedback for the attached proposal seems to be very positive.

Motivation

Property wrappers are currently limited to type properties. Use in function arguments or even closures is currently not possible leading to friction in development:

// Use in functions as an argument
func takeBinding(count: Binding<Int>) {
    count.wrappedValue = 1
    //    ^~~~~~~~~~~~ Introduces friction
}

// Use as an argument passed to a closure
func closureUsingBinding(
    _ block: (Binding<Int>) -> Void
) { ... }

closureUsingBinding { count in 
    count.wrappedValue = 2
    //    ^~~~~~~~~~~~ Introduces friction
}

We essentially see the problem property wrappers were introduced to solve in a slightly different form.

Potential Solution

Declarations

We could enable property wrappers in the above cases:

// Use in functions as an argument
func takeBinding(@Binding count: Int) {
    count = 1
    // 🙂 Natural use 
}

// Use as an argument passed to a closure
closureUsingBinding { @Binding count in 
    count.wrappedValue = 2
    // 🙂 Natural use 
}

Calling such functions (and closures)

takeBinding(count: $count)

func closureUsingBinding(
    _ block: (Binding<Int>) -> Void
) { 
    block($myCountBinding)
}

Some property wrappers however provide wrapped-property-style initializers: init(wrappedValue:). For such initializers convenience functions would be created:

func takeState(@State count: Int = 0) { ... }

takeState()
takeState(count: 1)
takeState(count: State(
    wrappedValue: 2
))

Of course, there are more complex cases:

@propertyWrapper
struct Wrapper<Value> {
    ...

    init(foo: String, wrappedValue value: Value) {
        ...
    }
}

func takeWrapper(
    @Wrapper(foo: "bar") wrapper: Int = 0
) { ... }

// Calling `takeWrapper`

takeWrapper()
takeWrapper(wrapper: 1)
takeWrapper(wrapper: Wrapper(
    foo: "bar", 
    wrappedValue: 2
))

But sometimes it makes sense for the wrappedValue not to be automatically provided:

func takeWrapper(
    @Wrapper(foo: "bar") wrapper: Int 
) { ... }

// Calling `takeWrapper`

takeWrapper(wrapper: 1) // `wrapper` is required
takeWrapper(wrapper: Wrapper(
    foo: "bar", 
    wrappedValue: 2
))

How would this work with mutable property wrappers?

@propertyWrapper
struct Mutable<Value> {
    var wrappedValue: Value
}

func takeMutable(@Mutable count: inout Int) {
    //                           ^~~~~ 
    // ✅ Allowed, since count is inout                 
    
    ...
} 

func takeMutable(@Mutable count: Int) {
    //                          ^~~~ 
    // ❌ `Mutable` requires that 
    //  count be marked inout.
  
    ... 
} 

Note that property wrappers like State will work without being mark inout:

func takeState(@State count: Int) { ... }

That behavior is intentional as State is actually not mutable. In fact, it's wrappedValue property is marked get and nonmutating set.

Transformations

As in the original proposal the compiler magic lies in the transformations. Currently, this:

struct Foo {
    @Mutable var bar: Int
}

is transformed into this:

struct Foo {
    var _bar: Mutable<Int>

    var bar: Int {
        get { 
            _bar.wrappedValue 
        }
        set { 
            _bar.wrappedValue = newValue 
        }
    }
}

Likewise, transformation would also occur with this solution:

Simple case

func takeBinding(@Binding count: Int) { 
    ...
}

Becomes:

func takeBinding(count _count: Binding<Int>) {
    var count: Int {
        get {
            _count.wrappedValue
        }
        nonmutating set {
            _count.wrappedValue = newValue
        }
    }
            
    var $count: Binding<Int> {
        _count.projectedValue
    }


    ...
}

Custom labels

func increment(@Binding by count: Int) { 
    ...
}

Becomes:

func takeBinding(by _count: Binding<Int>) {
    var count: Int { ... }
            
    var $count: Binding<Int> { ... }


    ...
}

So what's the behavior with labels?

The label written out by the user becomes the custom label and the property name is prefixed with "_".

Closure arguments

closureUsingBinding { @Binding count in 
    ...
}

Becomes:

closureUsingBinding { (_count: Binding<Int>) -> Void in 
    var count: Int { ... }
            
    var $count: Binding<Int> { ... }


    ...
}

Wrapped-value-type initializers

func takeState(
   @State count: Int = 0
) { ... }

Becomes:

func takeState(
    count _count: State<Int>
) {
    ...
}

// Synthesized function
func takeState(
    count: Int = 0
) {
    takeState(count: State<Int>(
        wrappedValue: count
    ))
}

Complex cases

func takeWrapper(
   @Wrapper(foo: "bar") wrapper: Int = 0
) { ... }

Becomes:

func takeWrapper(
   wrapper _wrapper: Wrapper<Int>
) {
    var wrapper: Int { ... }

    ...
}

// Synthesized function
func takeWrapper(
   wrapper: Int = 0
) {
    takeWrapper(Wrapper<Int>(
        foo: "bar",
        wrappedValue: wrapper
    ))
}

Complex cases with no provided wrappedValue

func takeWrapper(
   @Wrapper(foo: "bar") wrapper: Int 
) { ... }

Becomes:

func takeWrapper(
   wrapper _wrapper: Wrapper<Int>
) {
    ...
}

// Synthesized function
func takeWrapper(
   wrapper: Int // There is no `= 0` here, 
                // so specifying `wrapper`
                // is mandatory
) {
    takeWrapper(Wrapper<Int>(
        foo: "bar",
        wrappedValue: wrapper
    ))
}

Binding is a poor motivation for this feature. Most of the time you only mutate the wrapped value, and not the binding itself. So forcing the caller to use Binding is unnecessarily restrictive when you can just normally use inout, which already works with Binding:

struct Foo {
    @Binding var value: Bool
    init() { _value = .constant(false) }
}

func mutate(_: inout Bool) { ... }

let foo = Foo()
mutate(&foo.value) // ok
Terms of Service

Privacy Policy

Cookie Policy