Pitch: Property Delegates

I don't think we get to define "macro" however we want. There is significant prior art in other languages (Scheme, Rust, Scala, ... even C) for what a "macro system" should accomplish. Property delegates aren't doing a fraction of that, and we shouldn't pretend that do.

Existing precedent is a great point in favor of the #Foo syntax for using property delegates.

Oh, that doesn't work for technical reasons (the autoclosure can't return an inout, so you would be unable to forward a setter). Something more like the keypath formulation that's been discussed elsewhere in this thread gets closer to solving this problem (but has problems with exclusivity---no silver bullet here).

I think the "expose it as a separate decl" part is what makes this feel like a macro expansion rather than a magical language feature.

Doug

2 Likes

I'll mention that I'm concerned about using the #Foo syntax because it makes it harder to add other things like #if, #sourceLocation, #selector, and #available in the future. I'd rather keep prefix # as something for explicit compiler features that don't deserve full-on keywords, rather than have it sometimes be that and sometimes take an "argument".

10 Likes

To be fair that did not stop the maybe minor maybe not change of meaning of do (do - while —> repeat - while as do was used for do - catch instead of try - catch). We were able to move past it and we could for the name macro perhaps.

Changing a the spelling of a keyword is pretty different from changing the meaning of an entire concept. Macros are very similar across languages in purpose, while loops have a wide range of different spellings.

1 Like

I really don’t think property delegates are the right way to address encoding behaviors. Property delegates are primarily an abstraction of storage (access to storage, location of storage, etc). Encoding and decoding are not about storage but about an external representation of the value.

I am also not convinced that Equatable and Hashable should be defined in terms of the delegate rather than the value of the primary property. I see the point that there may be an opportunity for an optimized implementation in a delegate but am unsure of the implications of this approach. I think in general I would find it pretty surprising if using a delegate for a property resulted in a change to the semantics of its conformance to Equatable or Hashable.

4 Likes

First of all, your CustomStorage<T: Hashable> example already works with the proposal as written. It's fine to have requirements on the type parameter of a delegate type.

The MyStorage example could be made to work by loosening the rules, but the use case feels contrived. Is this really a delegate that cannot be abstracted in any meaningful way? Is it worth having two ways to spell property delegates to cover such a use case?

Doug

You didn't directly comment on my reply justifying the single-type-parameter restriction, but I'll try to reply to your specific comments below without duplicating that.

Neither of those declarations above are better than what is in the proposal:

  var property_as_proposed: T by DelayedMutable

Your first (property_1) hides the type of the property (T) within the delegate type. That's hiding the most important part of property_1 from the user.

Your second (property_2) duplicates the type T, and not in a way that brings any additional clarity to the code.

It is possible, yes, but it makes the model more complex, and all of the examples given so far have been theoretical or contrived. I don't feel that the complexity is warranted at this stage.

I can include it in the future directions.

Doug

It‘s true that I didn‘t commented to it directly, I read it and I respect your view point, but I think you missed my point here. With these variations I‘m not trying to make things complex but saying that these must be valid as this would be already regular Swift if we had the by syntax. It‘s not a question about clarity or duplication here but about consistency in the language. property_1 is basically what you have in your proposal as var property by Lazy = 17 but with explicit type instead of inference through init(initialValue:) and I didn‘t provide any default value. From the original proposal example I think this form should imply as valid.
In fact it was you who told me that you regret that Swift allow us to omit the generic type parameter list like for example when extending Dictionary, and now we‘re again in the same situation and trying to do the exact same thing.
Don‘t get me wrong here ;) if you want to omit the single type parameter, fine, but the explicit form must be possible if ever we need it for disambiguation, as the compiler not always doing what we expect.

Okay that‘s fair. Another possible future direction then?

It's probably not worth the complexity.

I strongly second this! I think encoding behaviour is better handled by annotations.

I'm not sure if that would be useful for the community but I found that maybe this pitch could be used to resolve one of the features that I would personally find sometimes handy.
Property delegates could be used to decide on the runtime who should own an object instead of explicitly decide it during type definition.
The problem is how we could pass more information besides the initialValue.

@propertyDelegate
class AdjustableOwner<Value: AnyObject> {
  enum OwnageType {
    case strong
    case weak
    case unowned
  }

  private let type: OwnageType
  private var _strongValue: Value
  private weak var _weakValue: Value!
  private unowned var _unownedValue: Value!

  init(initialValue: Value, ownageType: OwnageType = .strong) {
    self.type = ownageType
    switch ownageType {
    case .strong:
      _strongValue = initialValue
    case .weak:
      _weakValue = initialValue
    case .unowned:
      _unownedValue = initialValue
    }  
  }

  var value: Value {
    get {
      switch type {
      case .strong:
        return _strongValue
      case .weak:
        return _weakValue
      case .unowned:
        return _unownedValue
      }
    }
    set {
      switch type {
      case .strong:
        _strongValue = newValue
      case .weak:
        _weakValue = newValue
      case .unowned:
        _unownedValue = newValue
      }
  }
}

class A { }

class B { 
  var a: A by AdjustableOwner

  init(a: AdjustableOwner<A>) {
    self.$a = a
  }
}

let a = A()
let b = B(a: a) // strong by default
let b = B(a: a|strong)
let b = B(a: a|weak)
let b = B(a: a|unowned)
2 Likes

I am glad this is coming back up!

Overall the way I see this is that you are introduction two new concepts to swift.

  1. Direct store backing $somepropety access using the $
    I love the idea of getting access to the store backing of a property using $.
    Maybe I missed it but am I going to be able to access this with any property?
    The only other place where we using $ is for tuples $0, $1 etc. Could I use $self to reference the tuples itself? Would $self be valid?

  2. Some form of attributes specifically for properties using by
    I am not to excited about using by because I think it will conflict with custom tributes in the future. Would you consider introducing custom attributes first even if they are only able to define Property delegates at first?

I'm not sure which of my regrets you're referring to. I'm very happy with our decision to have the generic type parameters in scope in an extension, e.g.,

extension Dictionary {
  // can refer to Key and Value here, but I'm happy with that
}

It makes it clear that the names of generic parameters are part of the API, which I feel contributes to better API design.

I'm happy with the ability to omit generic parameters when they're inferred, e.g.,

let d: Dictionary = [1: "Hello"]  // infers Key=Int, Value=String, and I'm happy with that

Property delegates is effectively using that feature for the var x by Lazy = 17 case.

One thing I regret is being able to refer to Dictionary without generic parameters within the Dictionary definition or its extensions even when there is no inference. It collides with inference, and SE-0068's use of Self is so much smarter. I think my (bad) decision to allow the following may have predated the existence of Self in the language. I hope it did, because then I'd have some historical cover for my mistake ;)

extension Dictionary {
  func equals(_ other: Dictionary) -> Bool {  // same as Dictionary<Key, Value>, which was a Bad Idea
  }
}

With the current proposal, you don't need disambiguation because there is ambiguity: the original property type (e.g., the Int in var x: Int by Lazy) is plugged in as the generic argument to the delegate type.

Doug

1 Like

That is exactly what I meant. ;)

Yet still I don't understand the resistance of not allowing us to type out the full property delegate type. The proposal explicitly says that we can omit it, so people who prefer that can just omit the parameter list.

The only other case we have in the language where there is a backing stored property that's implicitly generated by the compiler is lazy. I suppose we could expose it (it has optional type) this way, but extending the feature set of the built-in features within the property delegates proposal seems a bit strange.

I couple of people have mentioned this, and it's worth investigating because it could pull together a couple of desired features into a single conceptual framework. I'm going to take a shot at implementing the @Lazy var x = 17, @UserDefaults(key: "key", default: true) var isSomething: Bool syntax implied by the custom-attributes approach before I make any conclusions here.

Doug

1 Like

I'm resisting because I don't want to introduce two ways to do the same thing without solid use cases for the second mechanism, because it complicates the description of the feature.

Doug

1 Like

Okay that's a fair argument. I want to ask you or in general the core team if you/they would think that it would be reasonable to allow non-generic or generic types with more type parameters as property delegates in the future?

Sorry if I'm being quite persistent on that one.

Speaking personally, it's a possible avenue for future extension, assume the design is good and supported by enough compelling use cases to motivate extension to the language.

Doug

1 Like

I have one other question that I'd like to ask. Assuming the proposal went though the review and is accepted. Will the stdlib extended with a set of (useful/common) property delegates (which the stdlib itself make use of)? In separate proposal of course.

2 Likes

This would make an excellent follow-on proposal.

Doug

3 Likes