SE-0258: Property Wrappers (third review)

+1 for the current revision. Thanks for the hard work in getting to this point.


A thought occurred to me on the topic of composition. With concepts separating underlying storage (_foo) from projection ($foo), I wonder if this provides an avenue for an explicit solution to the composition problem.

Suppose we have a @propertyWrapper struct Composite<T, U> which exposes each underlying wrapper as properties on its projection?

So something like:

@Composite(Binding, Bar) var foo = 1

Now immediately, there are issues with this approach:There's no protocol to constrain the composed wrappers on, and without Variadic Generics there's a static limit to the number of composed wrappers anyway, but I wonder if someone smarter could make a similar conceptual approach work, or if this is something we could solve by extending the feature in the future.

But if something like that were possible, it could yield explicit structured access to the underlying wrappers without the nesting order ambiguities that were evident in previous composition concepts:

print(foo) // 1
print(_foo) // the `Composite` storage
print($foo) // the `Composite` projection

$foo.$bar // the `Bar` projection 
$foo.$binding // the `Binding` projection

Or maybe it's just a dumb idea :sweat_smile:

@Douglas_Gregor is this a bug? I would assume that initialization through static functions should be possible:

import SwiftUI

struct Test {
  @Binding.constant(true) // error: Unknown attribute 'Binding.constant'
  var boolean: Bool

  // translates to
  private var _boolean = Binding<Bool>.constant(true)
  var boolean: Bool { ... }
}

I get that some people feel that the $foo syntax isn’t “swifty”, but I think this is just because we haven’t gotten used to it yet.

Some people have suggested things akin to #storage(of: foo). I guess that is just as “swifty”.

We already have \.foo and not #keypath(to: foo). And #"raw string" and not #raw("raw string"). And we use @available(...) and not #attribute(available, ...).

Point is: We have a lot of “magic” syntax already. I think this is fine.

3 Likes

This would also be a bit confusing since foo is just an Int or something, so either sourcekit would have to start differentiating between normal Ints and Ints that are backed by storage and only let us apply #storage to the latter type, or #storage would have to return an optional, or crash if used on a normal Int. Either of those solutions I feel would be worse, it is not that confusing to imagine the compiler generating the $foo property for us, and once we accept that, nothing in its behaviour will be magic.

1 Like

Actually this would be _foo and not $foo, which looks even less magic.

The code is ill-formed because it doesn't match the grammar for custom attributes specified in the proposal.

Doug

Sorry I had a small typo in the translated example. Was that the ill-formed part of the code or is this in general nothing we do want support? If this a limitation of the initial implementation, I‘m fine with that, but I would hope that we do eventually want to support intialization of the backing storage through some static members (factory pattern).

Hi all,

I just want to ask confirmation about the ability to mix initialisation of the wrapper via initialValue on the property's type + the rest of the wrapper's properties via the wrapper attribute itself.

This is a case that is mentionned in the Clamping example in the proposal:

Most interesting in this example is how @Clamping properties can be initialized given both an initial value and initializer arguments. In such cases, the initialValue: argument is placed first. For example, this means we can define a Color type that clamps all values in the range [0, 255]:

struct Color {
  @Clamping(min: 0, max: 255) var red: Int = 127
  @Clamping(min: 0, max: 255) var green: Int = 127
  @Clamping(min: 0, max: 255) var blue: Int = 127
  @Clamping(min: 0, max: 255) var alpha: Int = 255
}

But I don't see any explicit mention of it anywhere else (and especially not in the paragraph about initialization).


Given the number of times this proposal has be re-worked (and thanks to Doug again for the awesome work on it btw!!), and the length and details of this proposal, I can easily imagine that some examples could have been missed being properly updated to match the changes between each revision (we're only human after all), so I'm just hoping that this is not such a case of an old feature from which only an old version of the examples remain

Can we get confirmation that that this feature will still be supported? If so, I think it would benefit having some dedicated paragraph in the initialisation section of the proposal, explaining this ability and its constraints (initialValue parameter having to be first etc)

Thx!


[EDIT] After re-reading the proposal, I see there might some allusions of this ability in "Type inference with property wrappers" too, where it's mentioned A(initialValue: E, argsA...), showing initialValue as first argument. But that wasn't clear to me at first read that this was related, especially since this is the section about type inference and "if the first property wrapper type is generic", while I think this rule should not be limited to wrappers that use generics.
E.g. if I implement a non-generic CodableDate wrapper taking an init(initialValue: Date, formatter: DateFormatter), there's no generics involved but the ability to do @CodableDate(formatter: x) var date = Date() should still exist, so this still needs clarification imho :wink:

This works in Xcode 11 beta 3 for me:

@propertyWrapper
struct Wrapper<Value> {
  var wrappedValue: Value
  var projectedValue: String
  
  init(initialValue: Value, projectedValue: String) {
    self.wrappedValue = initialValue
    self.projectedValue = projectedValue
  }
}

struct Test {
  @Wrapper(projectedValue: "Swift")
  var property = 42
  
  func test() {
    print(property, $property)
  }
}

let test = Test()
test.test()

But $property has a wrong type since the current snapshot does not reflect the latest changes in the compiler.

1 Like

That's reassuring that it works, thanks for testing this in b3! :+1:

Still, since to me it's ambiguous in the official proposal, I think clarification about it in the dedicated paragraph about initialisation would help :slightly_smiling_face: (especially given that this proposal has evolved a lot so we have no guarantee that what is implemented in b3's compiler and what gets accepted in the upated proposal might differ, as you pointed out yourself :stuck_out_tongue_winking_eye:)

We have three options:

  • Create a PR and fix that paragraph
  • Wait until Douglas updates it (if at all)
  • Download and test the latest snapshot and file bug reports if something expected does not work.

I'm all ok for creating a PR myself to make it more clear; just needed to be sure what's expected first, so I document the right expected behaviour :wink:

It was intentional that it is limited to the initializer form (not referring to static members), because it provides a clearer grammar and doesn't admit funny business where, e.g., the static member returns something of a different type.

This restriction could be lifted in the future without breaking source compatibility (you're turning invalid code into valid code).

Doug

2 Likes

Well as long as the root type (property wrapper type) matches with the type returned by the last expression component which also will match with the wrapped property type, then we should be fine. ;)

But I‘m okay to defer that to a future proposal.


I‘ve seen that you merged a prototype of self injection already. Since this is part of the future direction and the design might require an extra proposal, can we avoid the same situation we have with function builders? By that I mean that the final design of that extension might be different than you currently implemented using a static subscript and a generic EnclosingSelf. What does it mean if apple frameworks use that hidden feature today? How can we avoid source / abi breakage?

1 Like

As long as that feature is not exposed outside Apple framework, this should not be an issue.
Maybe Apple has some internal policy preventing this, but technically Apple frameworks are not required to used only ABI stable functionalities as they are only mean to work with a single runtime and standard library version (they are distributed with the OS, alongside the runtime and standard library).

That said, if they start to use it for public PropertyWrapper classes, that would be an issue.

My point is that there is no way to deploy back any functionality that does not exist in the OS due to things like ABI stability. That means if apple frameworks use some swift internals, these things will stick with us forever because they cannot be swapped out on older OS‘s, which also could force us towards a design that is not ideal anymore (see how $ prefix was forced and how function builder design was kept for SwiftUI). Don‘t get me wrong, all I want to say is that I hope that these internals will be implemented as hooks that apple can use as they wish but that it does not effect any future direction or design of generalized features.


Which rdar is this? Will any public wrapper type use that feature?

The community was not yet invited to talk about and design that particular part of the wrapper feature.

1 Like

3 posts were split to a new topic: Question about nested property wrappers

We've been careful to make the use of these experimental features not tie our hands with respect to ABI, typically through the use of @_alwaysEmitIntoClient. We may have to carry some older entry points forward in SwiftUI, but that doesn't affect Swift's evolution.

It's a prototype of this much-requested future direction. It's a pure extension we should discuss separately.

Doug

1 Like

SE-0258 has been accepted with modification. If you'd like to continue discussing this, please do so in that thread; I'll be closing this one.

1 Like
Terms of Service

Privacy Policy

Cookie Policy