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

We have something in our app to customize Codable behavior for things like Date:

public struct CodableDate<Formatting: DateFormatting>: Codable, Equatable {
    public let date: Date

    public init(_ date: Date) {
        self.date = date
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let dateStr = try container.decode(String.self)
        guard let date = Formatting.formatter.date(from: dateStr) else {
            throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date format: \(dateStr)")
        }
        self.date = date
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(Formatting.formatter.string(from: date))
    }
}

I was trying to check if it would be possible for this to be a property wrapper, but run into some issues (I've skimmed through the proposal and didn't find any mentions to them):

  • Property cannot be declared public because its property delegate type uses an internal type: I had to make the @_propertyDelegate type public, but I'm not sure why this is required
  • Property delegate can only be applied to a 'var': our models use let properties
  • Is there a way to restrict a wrapper to only certain types? In my case, it'd like to restrict it to only Dates. I've tried declaring a Bool variable using my wrapper, but got a SIL verification failed: return value type does not match return type of function: functionResultType == instResultType, so I guess I should get a proper compilation error when this is approved/fully implemented?
  • Even after fixing these issues, I couldn't make it work on a model that implements Codable, so not sure what I'm doing wrong here:
@objcMembers
public final class Message: NSObject, Codable {
    public let id: Int
    @CodableDateWrapper<ISO8601FullFormat>
    public var createdAt: Date

    public init(id: Int, createdAt: Date) {
        self.id = id
        self.createdAt = createdAt
    }
}

@_propertyDelegate
public struct CodableDateWrapper<Formatting: DateFormatting>: Codable {
    public var value: Date

    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let dateStr = try container.decode(String.self)
        guard let date = Formatting.formatter.date(from: dateStr) else {
            throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date format: \(dateStr)")
        }
        self.value = date
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(Formatting.formatter.string(from: value))
    }
}


public protocol DateFormatting {
    static var formatter: DateFormatter { get }
}

public struct ISO8601FullFormat: DateFormatting {
    public static let formatter: DateFormatter = .iso8601Full
}
6 Likes

value does not need to be a var, it‘s okay to have let value: Date which would restrict your wrapper type to only Date properties. The wrapper itself must be applied to a computed property which implies the requirement of var.

However, a computed property can be readonly. Is there a way to have the same behavior with a property delegate?

If your property wrapper type has a readonly value (computed readonly, or a let), the wrapped computed property will also be readonly. The rule of thumb is that wrapped computed properties will get a synthetized accessors to its wrappers value and therefore mirror the set of accessors.

Did you meant to ask for that?


Also the very first issue with public/internal feels like a bug. @Douglas_Gregor can you verify this? The wrapper is private by default, and you want only to expose the computed property while hiding the backing storage. It makes no sense to require the wrapper type to be public as well.


Also I think this is a great example as it demonstrates that property wrapper can be (non-)generic, but value‘s type does not have to be a type from the generic type parameter list. @marcelofabri mind if @Douglas_Gregor would include a simplified version of it into the proposal?

@anandabits @Douglas_Gregor allow me to bikesheed for a moment about the justification of wrapperValue.

What if we could use a property wrapper in two ways.

  • the proposed way
  • explicitly using the wrapper type in a stored property without any $ need
@propertyWrapper
struct Email {
  var value: String {
    /* some regular expression for validation */
  }

  var wrapperValue: String {
    get { return value }
    set { value = newValue }
  }
}

public struct Foo {
  // proposed way
  @Email
  public internal(wrapper) var plainStringAsEmail: String

  // a different way
  public var email: Email
}

var foo = Foo(...)
let string: String = foo.email 
foo.email = "someone@apple.com"

It would allow custom newtype like types (like in previous discussions about newtype alternative to typealias).

We could also remove wrapperValue from property wrapper proposal and introduce it in a standalone pitch that would require a new attribute like @typeWrapper that would play along with property wrappers just fine.

What do you think?

Thanks, that makes sense.

However, because it's a computed property, I can't set it on init. Shouldn't this be automagically done by the compiler? I can init the underlying $date instead, but that is suboptimal.

I'm also not sure how this plays with Codable. I think the Codable mechanism will try to use the $variable, but you can't declare a CodingKey starting with $ to override it. If I use variable as a CodingKey, the compiler says it doesn't match any stored properties.

I'd love to see my example in the proposal, but I want to make sure it can work first.

Can you try a coding key with backticks and see if the it works then, otherwise it‘s a very good question that @Douglas_Gregor needs to answer.

// something like
case `$property_name`

Edit: I looked up the PR, Codable related changes are fairly new and might not exist in the current snapshots as the PR was only recently merged.

Here is the related commit.

Mind scrolling above, I already replied to that to @masters3d. :slight_smile:

1 Like

The Future Directions sections contains the following:

By default, the synthesized storage property will have private access.

My test project, using the 5/29 snapshot, allows the following code:

class C {
    @StatefulWrapper var a: Int = 0
    @StatefulWrapper var b: String = ""
}

var c: C? = C()
c?.a = 47
c?.b = "_ yo _"

c?.$a.on(next: { print($0) }).add(to: lb)
c?.$b.on(next: { print($0) }).add(to: lb)
c?.$b.map({ $0.uppercased() }).on(next: { print($0) }).add(to: lb)

I am hoping the pitch is incorrect, and that the actual behavior is intended.

I’m not sure what the purpose of @typeWrapper would be. Can you elaborate on what you have in mind?

Your example could be written today if Email: ExpressibleByStringLiteral. Are you suggesting you want to be able to declare a property of wrapper type but work with it as if it had the wrapped type (i.e. assign a string variable, not just a literal)? That doesn’t make sense to me. It would violate the written type.

It isn’t clear to me what any of this has to do with motivating wrapperValue.

The examples in the proposal and the one by @Avi imply to me that a major utility of wrapperValue (that we have seen thus far at least) is not as much about hiding the wrapper as it is eliding it when it offers no useful functionality beyond the wrapped value and something else related to that value (an UnsafeMutablePointer or an Observable).

That said, in @Avi’s example hiding the wrapper is also important. You definitely do not want anyone copying that wrapper struct. If they did you would end up with two independent values with shared observers which is almost certainly not what anyone actually wants.

This is an important observation and is the kind of motivation I have been looking for. wrapperValue can remove the sharp edges that would otherwise exist in some kinds of wrapper structs by preventing copying.

1 Like

Yes I'm suggesting just that. IIRC this is also known as newtype Email = String.

Assuming that property wrapper proposal would not include wrapperValue. Then we could add an attribute @typeWrapper (strawman syntax) which would expect you to provide a wrapperValue property in your type and shadow the wrapper type with wrapperValue's type.

But again this was just bikeshedding, and maybe newtype is still superior to this idea.

I don't think that copying is a problem in my example. The observable types are classes, so all you get are copies of the references. There's not much harm there, other than accessibility. However, to fix that, we need the future direction which allows restricting access to the wrapper relative to the property.

This is not how newtype works in any language I know of. It creates a wrapper type that can selectively forward APIs to the underlying type. You can't just use the underlying type in place of the new type. The whole point is that they are distinct types. That's what makes it different than typealias.

4 Likes

The problem with copying your type is that it mixes value and reference semantics. I have seen production bugs caused by code that is almost exactly like your code. Here's an example (not using wrappers to keep it simple)

struct StatefulWrapper<V> {
    var value: V {
        didSet { observer.next(value) }
    }

    let observer = Observer<V>()

    init(initialValue: V) {
        value = initialValue
        observer.next(initialValue)
    }
}

var wrapped = StatefulWrapper(initialValue: 42)
wrapped.observer.on(next: { print($0) })
wrapped.value = 43 // prints 43

var copy = wrapped
copy.observer.on(next: { print("copy \($0)") })
copy.value = 44 // prints "44" *and* "copy 44"

wrapped.value = 45 // prints "45" *and* "copy 45"

The problem is that value is copied by value and becomes independent but as you pointed out observer has reference semantics. So it becomes shared by both copies of StatefulWrapper. All listeners will receive events when any copy is updated.

This is extremely surprising to many people and unlikely to ever be what anybody actually wants. The reason this motivates wrapperValue is that by hiding the instance of this struct these accidental copies are no longer an issue. We can use a thin struct as a property wrapper in use cases like this without introducing potential for unintentional misuse.

In my case, that's exactly the behavior I'd expect and want. Without the property wrapper, the instance property would be the observable itself, so accessing it would be effectively making a copy of the reference. The behavior is the same.

Note: I don't pretend that my use-case is typical.

This is how RxSwift / ReactiveSwift works. The observable types are classes and have reference semantics. To create complex sequences you share them or parts of them composed with different operator methods. I'd put my hands in fire and say that this is expected behavior. However I understand how with wrapperValue you could avoid this, because the compiler won't let you access the wrapper type anymore and you can only copy the value of wrapperValue.

Also keep in mind that if wrapperValue was present on StatefulWrapper, then you can no longer write code such as wrapped.observer (or $property.observer) as every access to the property wrapper will be routed through wrapperValue.

You want two totally independent values feeding changes into the same Observable? Why? What is the use case for that? As I said before, I have seen code like this cause bugs in production code.

I understand how they work very well and work with ReactiveSwift all the time. The problem I am pointing out is not reference semantics on its own. It is the coupling of reference and value semantics in a rather subtle way.

The most similar type to Avi's offered by ReactiveSwift is MutableProperty. Note that this is a class, not a struct. When you "copy" a MutableProperty you're only copying a reference. Using reference semantics here maintains the association of the change signal with a single backing value. This design avoids mixing value and reference semantics in a subtle and surprising way.

My point is that while expert users may understand how a type like StatefulWrapper behaves and therefore avoid surprises not everyone is an expert user. And even expert users make mistakes. Further, I don't know of any good use cases for allowing copies of a type like StatefulWrapper.

1 Like

I now understand the confusion. My library has an Observer type which cannot be fed values (through a public API), but it can itself be observed. Observable can be fed input, and the wrapper hides the Observable instance. Even copying the struct does not give access, so the only public API is for observing, and this is a desirable property.

Current Implementation
@_propertyDelegate
struct StatefulWrapper<V> {
    var value: V {
        didSet { observable.next(value) }
    }

    private let observable = Observable<V>()
    private let observer: Observer<V>

    var delegateValue: Observer<V> { return observer }

    init(initialValue: V) {
        value = initialValue
        observable.next(initialValue)
        observer = observable.observer()
    }
}

Right, but you're still copying the Observable by reference and the value by value. As far as I can tell the issue I'm describing would be reproducible with your library and StatefulWrapper.

Are we still considering global and local variables in the first version of this feature? The proposal explicitly mentions them, but actually trying to use them (in the latest snapshot from May 29th) on a global causes interesting missing symbol errors, while locals just say they aren't implemented yet.