Pitch: Property Delegates

That's not actually permitted syntax under this proposal; you can either explicitly initialize the delegate or you can provide an initial value of the "public" type with =, which gets turned into the call to init(initialValue:). So the "arbitrary code" part is always at the end of the declaration.

Ah, thanks, I didn't appreciate that when reading the proposal, and I thought it would initialise using the delegate's initialiser and then assign to value. Well, less potential for confusion but the delegate is still in the middle or at the end of the property declaration, which isn't consistent with the current similar features.

I really like the simplicity of this being just syntactic sugar for a wrapper type. Itā€™s a very straightforward approach and echoes the design for user-defined attributes that @hartbit recently suggested. Like some of the others, Iā€™m not sure of the usage syntax but discussion of that can wait.

This is just my immediate intuition without deep thought, but it seems like it might be a good idea to restrict @propertyDelegate to value types. Does anyone know of good use cases that would require @propertyDelegate class? Everything I can think of would use value types, even if a class was used in the implementation, for example:

@propertyDelegate
struct Indirect<Value> {
    private class Box { 
        var value: Value
        init(value: Value) { 
           self.value = value
        }

        func copy() -> Box {
             return box(value: value)
        }
    }

    let box: Box

    init(initialValue value: Value) {
        box = Box(value: value)
    }

    var value: Value {
        get { 
              return box.value 
        }

        set { 
           if !isKnownUniquelyReferenced(&box) { 
               box = box.copy() 
           }
           box.value = newValue
        }
    }
}

The above example makes me wonder if it would be possible to have general syntax for initializing a delegated property using an existing value of the delegate type. For example:

var int: Int by Indirect = 42

// this is only possible if the property delegate has a ā€œcopy initializerā€
var other1: Int by Indirect($int)

// this might be possible, but is probably not a great idea as $int is not of type Int
// is there an alternative that does not have potential for confusion or implementation
// complexity but also does not require manually writing a copy initializer?
var other2: Int by Indirect = $int

With respect to SE-0030, I especially miss its composability. I wonder if the direction @Paul_Cantrell suggested of using an additional generic parameter might be a viable way to approach composition. I would really like to see this direction explored further.

If composition via generics is viable I think it pushes us in the direction of using a PropertyDelegate protocol - we would need a way to constrain the wrapped property delegate and a protocol is the natural way to do that. This would necessarily be a ā€œmagicā€ protocol as it isnā€™t expressible directly in the language, but Iā€™m ok with that if it allows us to support composition of property delegates.

A property delegate that implemented atomics would need to use a class; it's the only way in current Swift to be sure you're not working on a temporary copy of the data. You could wrap the class in a struct, of course, but doesn't that just demonstrate that a "value type only" rule would be paper-thin?

1 Like

This would be a great feature! However:

  • I'm not a huge fan of the by syntax, as by has no obvious meaning there. Something more compact would probably be better, especially for types with a lot of delegated properties. Perhaps var[Lazy] string = "string"? Seems easier than a new keyword.
  • Is it at all possible to vend the delegate's API through the property? Would outside users have access to the $ value? I'm thinking about @jrose's Observable above. If I had a var[Observable] user: User, would there be a way to get access to the addObserver function? The example exposes a _ variable directly, so I'm not sure how it would map to the proposal. But this would be huge for creating observable properties.
  • Is the restriction on not having properties with delegates a limitation of delegates in general, or just this first proposal? If we can't declare delegates, would be be another type equivalence we could use? i.e. Could var[Observable] user: User be considered equivalent to var user: Observable<User>, at least in some contexts?
7 Likes

I have been waiting for this for years, thank you for bringing this up again.

I have one question regarding the mutability of properties with delegates. Is there any limitation that preview the properties to introduced with let keyword? I think there're a few cases where I would need to use the delegates for a constant let property, for example the lazy or delayed initialization.

3 Likes

This looks great, thanks!

One concern is that the composability has gone iiuc. So I can't specify e.g.

ā€¦ by Atomic, Lazy

So there will be a lot of Delegate types that just combine others? Got a new one ā€“ now you have to write 20 variants of it if you need other behavior composed (In reality I guess there will be a handful of common ones, so I am not sure this will be a big issue)

I agree with others here that the syntax is probably the thing to like least, but that's something for another discussion I think.

Use via instead of by (which would also be a lot easier to search for with relevant results)

or delegate in front: var(Lazy) kermit: Muppet

Anyway, again thank you.

7 Likes

I don't like the new syntax, nor the old one. I would prefer having it as attributes or something like what tkrajacic proposed:

3 Likes

@Douglas_Gregor thank you so much for pushing property behaviors forward, even now a slightly different form. I just (re-)read both proposals in comparison and I see the similarities and appreciate the work and time you spent on this.

Even though both Property Behavior and Property Delegate only aim to be citizen of a variable (expect the case of 'out-of-line initialization' where let can be treated like a var) I still think we need to explore the possibilities to allow some property delegates on constants. Such constants most likely will be, like @pitiphong.p mentioned, both lazy and delayed initialized or for example just delayed immutable.


I cannot wait to get my hands on DelayedImmutable as it eradicates the need of optionals on @IBOutlet properties and also guards the property from being re-assigned later on. Ideally however I would wish to be able to write something like @IBOutlet let label: UILabel by PropertyDelegate. That PropertyDelegate should do multiple things:

  • it should act like DelayedImmutable
  • it should guard in context of a UIViewController, the controller's isViewLoaded property returns true when accessed the label through a getter.

The second point is my next problem with the current design as it does not allow access to self of the property holder. Can we somehow loosen up this restriction?


I'm also not a huge fan of the by syntax and prefer something like property behaviors had, but I see the other use cases presented by property delegates which are not expressible otherwise.


Can we have multiple overloads of the delegate's initializer? As proposed a Lazy property can be written as: var foo: Int by Lazy = 42 but if instead of an Int you had a more complex type that requires more setup we could provide a closure instead of the value. It would be very convenient if we could avoid the need of () at the end of the closure. That said, I think to have an extra overload without @autoclosure on Lazy would be great. Such an overload should allow us to write this:

var label: UILabel by Lazy = {
  let label = UILabel()
  /* configure the label here */
  return label
}

Can you please explicitly mention in the proposal if this will be somehow backwards compatible with Swift 5?


Overall I'm happy to see progress on that area. Thank you.

3 Likes

One possibility here would be to have separate access control for foo and $foo. With an Observable thing you clearly want its nature exposed in your interface, but with a Lazy one you probably don't.

If we had protocol delegates, we could make an operator to compose the delegates:

Lazy + Observable

(the + operator is wrong because the operation is not commutative but I'm not sure what to put)

Here is an example of a property delegate I would want to express to get rid of the shortcomings and a lot of programmer mistakes in projects on apple platforms:

@propertyDelegate
struct DelayedImmutableView<View: UIView> {
  private unowned let _controller: UIViewController
  private var _value: View? = nil

  init(controller: UIViewController) {
  	self._controller = controller
  }

  var value: View {
    get {
      guard _controller.isViewLoaded else {
      	fatalError("controller's view must be loaded before accessing the view")
      }
      guard let value = _value else {
        fatalError("property accessed before being initialized")
      }
      return value
    }

    // Perform an initialization, trapping if the
    // value is already initialized.
    set {
      if _value != nil {
        fatalError("property initialized twice")
      }
      _value = newValue
    }
  }
}

Then I would write something a property like this:

@IBOutlet lazy var label: UILabel by DelayedImmutableView

It would translate into this:

lazy var $label = DelayedImmutableView<UILabel>(controller: self)
var label: UILabel {
  get { return $label.value }
  set { $label.value = newValue }
}

And can be safely used from here:

func viewDidLoad() {
  super.viewDidLoad()
  // No more optionals, no more silly mistakes updating a view
  // before the controller was woken from `Nib` where you just lose
  // that data.
  self.label.text = "Swift is awesome"
}

If we could also get rid of lazy here and create multiple delegates that are also composable that would be amazing. We can potentially make @IBOutlet into a property delegate as well and move it from the language surface into UIKit. But then we definitely need a way to compose multiple delegates var label: UILabel by [IBOutlet, DelayedImmutableView].


Sad story that @IBOutlet requires the type to be optional otherwise we could already use the above delegate type.

class Controller: UIViewController {
  private lazy var __label = DelayedImmutableView<UILabel>(controller: self)
  @IBOutlet var label: UILabel { // error @IBOutlet property has non-optional type 'UILabel'
    get { return __label.value }
    set { __label.value = newValue }
  }
}
4 Likes

This is one of those features that for day to day development would only come up occasionally, but when the need does arise it would be fantastic to have. It also will allow for some 'not so nice' rough edges we have now to be cleaned up, so generally I'm all for it.

I think I'm in the same boat about the syntax which some others have mentioned above. I'm not totally sold on the by keyword as it takes a bit longer to digest when simply glancing at the property declaration (you have to look backwards and forwards through the declaration to understand how it behaves), but I do quite like the var(Lazy) examples that have been proposed as an alternative. Generally when you are dealing with properties you look towards the beginning of the declaration for the behaviour of it (mutable/immutable), so by moving the property delegate to be part of let or var it's quicker to understand the overall behaviour of the property without having to look backwards and forwards through it.

Behaviour > Name > (Type) > Value
var(Lazy) foo: Int = 1738

vs

Mutability Behaviour > Name > (Type) > Delegate Behaviour > Value
var foo: Int by Lazy = 1738

2 Likes

I like the choice of not using protocols for the feature, but I have one concern:
To me, it's still completely unclear what kind of metaprogramming capabilities will be added to Swift (if any at all), so I have no idea how it may interfere with this pitch.
Most likely, there won't be a real conflict, but I can think of flexible solutions that would render property delegates superfluous.
Given the fact that due to compatibility concerns, it becomes harder and harder to remove legacy features, it might be a good idea not to spend significant resources to catch up with Kotlin.

I had no pressing need for neither property delegates nor advanced metaprogramming in the last years, so I'd rather wait a little bit longer if this patience had the prospect of a better solution.

1 Like

A lot of people are suggesting things they find "missing" from the proposal, and I want to point out that that's one of the reasons SE-0030 had to get sent back: no one was sure that what was there was the right set. If we can agree that the stuff that is here is good, we can expand later.

I'm not saying not to talk about "missing" things at all! As usual, that helps us as a community keep from boxing ourselves in with something that's not expandable. But if there's an at all plausible way to do access control, or references to self, or something sensible with let, then we don't have to do it now.

(That's also my complaint about a lot of the alternate syntax suggestions here: they're way less expandable, and already don't account for the by Foo(context: importantConfiguration) syntax that's included in the current version of the proposal. Not having that would force explicit initialization within types and reduce the applicability outside of types.)


Responding to some specific comments:

I hate to say it, but let + Lazy is likely never to happen, because Lazy inherently has a mutating getter. You can get the same effect with DelayedImmutable, but it's never going to be truly a let.

The "overload without autoclosure" example isn't really important; like today's lazy (or non-lazy), you can get the same effect by immediately calling the closure that computes the initial value. The compiler can in theory guarantee that that's just as efficient, though I don't know if it does so today.


I think Dale's comment sums up my overall opinion: "This is one of those features that for day to day development would only come up occasionally, but when the need does arise it would be fantastic to have."

8 Likes

That's not my main concern about this proposal, I don't want to support something that is not clearly specified. I asked for clarification that this feature is not restricted to one optional initializer that is set in stone, that only accepts one Value type and thats it. This is neither flexible nor in any case extensible from the current perspective. The proposal lacks clarifications where it ultimately wants to go in the long run. All I want to say here is that we as the community should 'together' carefully design and discuss this feature instead of going the 'let's ship the first bare-bone version' road and see what we can do with it later. That would be as if you'd put bricks on your road and then later have to build a bridge around it.

That said, accumulating examples and potential use-cases of this feature is a good thing and we should not push these things back so hard, but shape the overall feature from it. ;)

1 Like

I like the pitch! I'm not enamored of by, however.

Here are a few other thoughts:

// Original `by`:
var foo by Lazy = 1738
public var foo: Int by public Lazy = 1738
public var foo: Int by Lazy(closure: { 42 })

// `of`:
var foo of Lazy = 1738
public var foo: Int of public Lazy = 1738
public var foo: Int of Lazy(closure: { 42 })

// `with`:
var foo with Lazy = 1738
public var foo: Int with public Lazy = 1738
public var foo: Int with Lazy(closure: { 42 })

// `using`:
var foo using Lazy = 1738
public var foo: Int using public Lazy = 1738
public var foo: Int using Lazy(closure: { 42 })

I'm sort of liking using. Thoughts?

(edit btw: I see the syntax highlighter is highlighting using. Am I forgetting a current keyword using?)

5 Likes

I like the proposal, except for the by keyword. It doesn't read naturally to me. In the following code:

var x: Int by Lazy = 49

What is the intended meaning of by?

  • x is an Int by way of Lazy?
  • x is an Int created by Lazy?
  • x is an Int accessed by Lazy?

There's some ambiguity here, and I'm not quite sure how to read it.

Edit
Cross-posting with @jberry above. "using" also sounds more natural to me.

7 Likes

Just one thing I still would like to say. I respect everything @jrose wrote and I also agree in general with his core points. I could imagine what we could go one level deeper and not implement the whole range of features currently proposed. I'm actually intrigued by the $ prefixed storage syntax. Personally it would be fine by me to just push that part of the proposal and see how the community can shape it. Later we could add some nice syntactic sugar on top to make things more convenient. The storage re-direction could be made explicit for now and we could add some synthetization later on. Doing that this way would allow us to create more fine-grained storage delegates already with the cost of one extra line per property with a delegate.

protocol PropertyStorage {
  associatedtype Value
  var value: Value { get }
}
protocol MutablePropertyStorage: PropertyStorage {
  var value: Value { get set }
}

final class MyIntPropertyStorage: MutablePropertyStorage {
  ...
  var value: Int { ... }
  ...
}

//==-----------

// Explicitly written storage.
private let $x: MyIntPropertyStorage
// Let us just automatically synthesize the getter and setter
// to the delegate without exposing it.
public var x: Int // implicitly { get / read } or { get set / read modify }

public init() {
  self.$x = PropertyDelegate(...)
}

I think that trade-off would be worth it in the long run as it already eliminates many of the limitations of the current proposal.

1 Like

Here's one more alternate: adopting, which is quite searchable, I would think...

// `adopting`:
var foo adopting Lazy = 1738
public var foo: Int adopting public Lazy = 1738
public var foo: Int adopting Lazy(closure: { 42 })