[Pitch #3] Property wrappers (formerly known as Property Delegates)

In my example I meant to nest two property wrapper types and the wrapped property to be of type Value.

// both are equivalent
@A<B, Int, String> var property: Value
@A<B<Value>, Int, String> var property: Value

// the above forms allow us to avoid the following composition form.
@A<B, Int, String> 
@B
var property: Value

@A<B<Value>, Int, String> 
@B<Value>
var property: Value

FWIW, I'm opposed to changing the grammar of attributes to introduce commas. It won't be at all clear when commas are needed or significant, and I don't think it clarifies this code at all.

I've put my thoughts into the proposal draft, so I won't repeat them at length here, but I feel like commutative composition is a non-goal for property wrappers.

  • Doug
7 Likes

Right, that'd work. We can even add $$foo, $$$foo, etc. in the future to access inner wrappedValue should the need arise, though quite frankly I don't even see the need myself.

What about 'half-way initialization'? Would you be allowed to do this?

@MyPropertyWrapper(name: "test") var property: Int

init(property: Int) {
    self.property = property
}

I imagine this being similar to declaring a constant value and assigning it later, which is something we can already do.

If you can construct MyPropertyWrapper(name: "test"), this will work. We can't call an initialValue: initializer with no initial value, though.

Doug

What about using a function similar to type(of:) to access the wrapper? Something like wrapper(of:keypath:)

1 Like

Putting it first is fine. If this can be done, this resolves the last issue I have with the proposal :)

1 Like

For anyone following along on this thread... property wrappers is up for its second review at SE-0258: Property Wrappers (second review) .

Can I still post a question in this thread?

Regarding synthesized Decodable, I am trying to find a way to make an omitted key acceptable.

The following type will decode the JSON "{}" successfully:

struct OmitTest: Decodable {
    var val: String?
}

let val = try! JSONDecoder().decode(OmitTest.self, from: #"{}"#.data(using: .utf8)!)

However, I cannot think of a way to retain that synthesized behavior while adding a wrapper to the property val.

Given the trivial Property Wrapper

@_propertyWrapper
struct Omittable<T: Decodable>: Decodable {
    var value: T?

    init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()

        value = try container.decode(T.self)
    }
}

The following code exhibits the behavior I am looking for (without using the property wrapper as a property wrapper)

struct OmitTest: Decodable {
    var val: Omittable<String?>?
}

However, I cannot find a way to retain that decoding behavior when using it as an attribute

struct OmitTest: Decodable {
    @Omittable var val: String?
}

Is the above the intended behavior of synthesized decoding?

2 Likes

I just started playing around with property wrappers and I immediately tried to find a concise way to do the same thing that @Avi outlined. I think that it is pretty important that the initialValue initializer have a flexible number of additional arguments after it.

There's a subtle difference between your two examples:

// the former is
var val: Omittable<String?>?
// the latter is desugared as
var val: Omittable<String?>

Notice that the former is an optional omittable, whereas the latter is a non-optional omittable. Decoding a type with optional fields, will work with missing keys. In fact, you could just declare it as var val: String? and it would work just the same. In the second example, where you use the type as a property wrapper, decoding will fail because the key is missing.

I have experimented with creating a more specific overload of decode(_:forKey:) on KeyedDecodingContainer like so:

extension KeyedDecodingContainer {
  func decode<D: Decodable & ZeroInitializable>(
    _ type: Omittable<D>, 
    forKey key: Key
  ) throws -> Omittable<D> {
    return try decodeIfPresent(type, forKey: key) ?? type.init()
  }
}

To allow this kind of notation:

@Omittable var val: String

As long as String conforms to ZeroInitializable which is a custom protocol which defines a static "zero" value for a given type, eg. empty string, empty array, zero numeral, false, etc.

2 Likes

Spot on. I thought that my problem was there was no syntax to indicate that Decodable should treat @Omittable var val: String? as if it desugared to var val: Omittable<String?>?.

However, I'm really intrigued by your solution using an extension on KeyedDecodingContainer. I think that is workable, at least in my use-case. For my actual "omittable" type I will build in a Bool indicating whether the value it wraps was omitted and use your extension (although I do not even need ZeroInitializable because I can set Omittable's wrapped value to nil and its omitted Bool to true in Omittable.init()).

Thanks for the tip!

Sure! In my own experiments with this, I've used "omittable" as a kind of "default value decodable", in that it can decode if present, and fall back to a default "zero" value.

I would like to be able to use:

@Omittable(default: "some default") var val: String

But that doesn't really work when decoded, as the initializer on declaration isn't called in that case, and there's no way of getting at the parameter. In the current design it must be entirely type-based. It is, however, possible to wrap a value as a static member of a phantom type, like so:

protocol ZeroInitializable {
  associatedType ZeroType
  static var zeroValue: ZeroType { get }
}

enum False: ZeroInitializable {
  static let zeroValue = false
}
enum True: ZeroInitializable {
  static let zeroValue = true
}
enum Zero<N: ExpressibleByIntegerLiteral>: ZeroInitializable {
  static var zeroValue: N { return 0 }
}
extension String: ZeroInitializable {
  static let zeroValue = ""
}
extension Array: ZeroInitializable {
  static var zeroValue: [Element] { [] }
}

And then using it like so:

struct SomeDecodable: Decodable {
  @Omittable(False.self) var isEnabled: Bool
  @Omittable(True.self) var hasContent: Bool
  @Omittable() var name: String
  @Omittable(Zero<Int>.self) var count: Int
}

Where the initialiser used on the declaration site, is only used to infer the type, and where the actual value used as a default value, is available through the phantom type on the wrapper.

I do a lot of phantom type trickery of a similar nature in a JSON:API library I wrote (not adapted to use property wrappers yet, but that’s the basis for my current experimentation). This is exactly the motivation for my suggested future direction in the new proposal thread for this feature.

@Published could not be used on static property of class.

public class UserRecord {
@Published
static var defaultUserRecord : UserRecord? = nil
}

Compile Error: Class stored properties not supported in classes; did you mean 'static'?
iOS beta 2

could not access wrapped value using @Published.

public struct UserRecordCenter {
@Published
public static var defaultUserRecord : UserRecord? = nil
}

Compile Error:
$defaultUserRecord is inaccessible due to 'internal' protection level

when $defaultUserRecord is inaccessible due to 'internal' protection level