SE-0030 Property Behaviors


(Anton Zhilin) #1

First of all, I insist that a deadline of less than 1 week is extremely
tough for this specific proposal. There are just too many details that we
need to work out, too many approaches to the problem that we want to
consider. As many people as possible should be able to express ideas on
this. A special request to core team is to extend the review to 2 week at
least, or even more if it will be needed.

I dislike the proposal as it is, especially the syntax. I think that
rolling out a language feature and "bikeshedding" it is only right if we
are ready to settle with most (although not all) of it. It's not the state
of things currently.

I have prepaired a draft (not real proposal) of my vision on the problem. I
tried to look at it from the other side, using existing Swift terms where
possible. Some wording or details might be off. Link to the gist:

https://gist.github.com/Anton3/f71a3e2ee29dffe1b9b2


(Joe Groff) #2

First of all, I insist that a deadline of less than 1 week is extremely tough for this specific proposal. There are just too many details that we need to work out, too many approaches to the problem that we want to consider. As many people as possible should be able to express ideas on this. A special request to core team is to extend the review to 2 week at least, or even more if it will be needed.

I'm inclined to agree. I'd be happy to extend the review period.

I dislike the proposal as it is, especially the syntax. I think that rolling out a language feature and "bikeshedding" it is only right if we are ready to settle with most (although not all) of it. It's not the state of things currently.

I have prepaired a draft (not real proposal) of my vision on the problem. I tried to look at it from the other side, using existing Swift terms where possible. Some wording or details might be off. Link to the gist:

https://gist.github.com/Anton3/f71a3e2ee29dffe1b9b2

public behaviour lazy<ValueType> {
    private var storage: ValueType? = nil
    private var closure: (() -> ValueType)?

IMO it's unacceptable to have to store a closure for every individual lazy property. That significantly increases the storage cost of the abstraction.

-Joe

···

On Feb 12, 2016, at 12:08 PM, Антон Жилин <antonyzhilin@gmail.com> wrote:


(Chris Lattner) #3

Joe, are you planning to send out another draft of the proposal? Maybe you should do that and extend the period at the same time,

-Chris

···

On Feb 12, 2016, at 12:44 PM, Joe Groff <jgroff@apple.com> wrote:

On Feb 12, 2016, at 12:08 PM, Антон Жилин <antonyzhilin@gmail.com <mailto:antonyzhilin@gmail.com>> wrote:

First of all, I insist that a deadline of less than 1 week is extremely tough for this specific proposal. There are just too many details that we need to work out, too many approaches to the problem that we want to consider. As many people as possible should be able to express ideas on this. A special request to core team is to extend the review to 2 week at least, or even more if it will be needed.

I'm inclined to agree. I'd be happy to extend the review period.


(Joe Groff) #4

Yeah, I'll do that.

-Joe

···

On Feb 12, 2016, at 1:28 PM, Chris Lattner <clattner@apple.com> wrote:

On Feb 12, 2016, at 12:44 PM, Joe Groff <jgroff@apple.com <mailto:jgroff@apple.com>> wrote:

On Feb 12, 2016, at 12:08 PM, Антон Жилин <antonyzhilin@gmail.com <mailto:antonyzhilin@gmail.com>> wrote:

First of all, I insist that a deadline of less than 1 week is extremely tough for this specific proposal. There are just too many details that we need to work out, too many approaches to the problem that we want to consider. As many people as possible should be able to express ideas on this. A special request to core team is to extend the review to 2 week at least, or even more if it will be needed.

I'm inclined to agree. I'd be happy to extend the review period.

Joe, are you planning to send out another draft of the proposal? Maybe you should do that and extend the period at the same time


(Joe Groff) #5

I like Anton’s proposal much better! Clean, obvious and down to the point.

IMO it's unacceptable to have to store a closure for every individual lazy property. That significantly increases the storage cost of the abstraction.

-Joe

Maybe I am missing something obvious here but what would be a practical example of a lazy variable that does not rely on a closure to provide the initial value? Besides, storing an additional pointer per property is what, 8 bytes overhead? Barely worth mentioning. And of course, if you want to be very efficient about it you can always use a global hash map to store the closures.

A Swift closure is two pointers wide—a function pointer, and a context pointer. The per-instance overhead for Optional<T> will already cost a word for many types without an extra bit or representation for 'None'.

BTW, the lazy implementation you propose also has additional overhead, but its hidden (initialValue needs to be stored somewhere before the first call to get). In fact, your solution might be even worse in terms of storage overhead, because it implies that individual closure with unique environment needs to be created for getters of every instance of the property with different initialiser.

As currently implemented, 'lazy' inlines the initializer expression into the property's getter implementation, so the only overhead is some code size in the getter function. A behavior implementation of [lazy] needs to afford the same opportunity to the optimizer. One way we could model this is as 'static' members in the behavior, perhaps.

-Joe

···

On Feb 12, 2016, at 1:45 PM, Taras Zakharko <taras.zakharko@uzh.ch> wrote:

On 12 Feb 2016, at 21:44, Joe Groff via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:


(Taras Zakharko) #6

I like Anton’s proposal much better! Clean, obvious and down to the point.

IMO it's unacceptable to have to store a closure for every individual lazy property. That significantly increases the storage cost of the abstraction.

-Joe

Maybe I am missing something obvious here but what would be a practical example of a lazy variable that does not rely on a closure to provide the initial value? Besides, storing an additional pointer per property is what, 8 bytes overhead? Barely worth mentioning. And of course, if you want to be very efficient about it you can always use a global hash map to store the closures.

BTW, the lazy implementation you propose also has additional overhead, but its hidden (initialValue needs to be stored somewhere before the first call to get). In fact, your solution might be even worse in terms of storage overhead, because it implies that individual closure with unique environment needs to be created for getters of every instance of the property with different initialiser.

— Taras

···

On 12 Feb 2016, at 21:44, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

On 12 Feb 2016, at 21:44, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

On Feb 12, 2016, at 12:08 PM, Антон Жилин <antonyzhilin@gmail.com <mailto:antonyzhilin@gmail.com>> wrote:

First of all, I insist that a deadline of less than 1 week is extremely tough for this specific proposal. There are just too many details that we need to work out, too many approaches to the problem that we want to consider. As many people as possible should be able to express ideas on this. A special request to core team is to extend the review to 2 week at least, or even more if it will be needed.

I'm inclined to agree. I'd be happy to extend the review period.

I dislike the proposal as it is, especially the syntax. I think that rolling out a language feature and "bikeshedding" it is only right if we are ready to settle with most (although not all) of it. It's not the state of things currently.

I have prepaired a draft (not real proposal) of my vision on the problem. I tried to look at it from the other side, using existing Swift terms where possible. Some wording or details might be off. Link to the gist:

https://gist.github.com/Anton3/f71a3e2ee29dffe1b9b2

public behaviour lazy<ValueType> {
    private var storage: ValueType? = nil
    private var closure: (() -> ValueType)?

IMO it's unacceptable to have to store a closure for every individual lazy property. That significantly increases the storage cost of the abstraction.

-Joe
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Taras Zakharko) #7

Ok, fair enough. Then here a few ideas how to fix it and still stay in-line with Anton’s proposal (that i still consider much more attractive):

1. Allow behaviours to specify abstract members that need to be implemented (akin to protocols) and get the initial value of lazy from there, e.g.:

    var lazy x : T {
  func load() {
    return 33
  }
    }

  Pros: clean and clear. Cons: verbosity.

2. Allow behaviours to have members stored per property declaration and not per property instance. These members are tied to the type-level storage of the host item (e.g. class and not instance). A closure that initialises the lazy storage can be stored at that level, thus solving the storage overhead issue. One can use the static declarations for this (although a new storage class might be appropriate). Note that Python uses this kind of approach per default: instances of property descriptors are created per class that hosts a property and not per instance. The property getter/setter then receives the specific object instance to manipulate the value specific to that instance.

Pros: clean and clear. Cons: potentially need new storage declaration.

— Taras

···

On 12 Feb 2016, at 22:59, Joe Groff <jgroff@apple.com> wrote:

On Feb 12, 2016, at 1:45 PM, Taras Zakharko <taras.zakharko@uzh.ch <mailto:taras.zakharko@uzh.ch>> wrote:

I like Anton’s proposal much better! Clean, obvious and down to the point.

On 12 Feb 2016, at 21:44, Joe Groff via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

IMO it's unacceptable to have to store a closure for every individual lazy property. That significantly increases the storage cost of the abstraction.

-Joe

Maybe I am missing something obvious here but what would be a practical example of a lazy variable that does not rely on a closure to provide the initial value? Besides, storing an additional pointer per property is what, 8 bytes overhead? Barely worth mentioning. And of course, if you want to be very efficient about it you can always use a global hash map to store the closures.

A Swift closure is two pointers wide—a function pointer, and a context pointer. The per-instance overhead for Optional<T> will already cost a word for many types without an extra bit or representation for 'None'.

BTW, the lazy implementation you propose also has additional overhead, but its hidden (initialValue needs to be stored somewhere before the first call to get). In fact, your solution might be even worse in terms of storage overhead, because it implies that individual closure with unique environment needs to be created for getters of every instance of the property with different initialiser.

As currently implemented, 'lazy' inlines the initializer expression into the property's getter implementation, so the only overhead is some code size in the getter function. A behavior implementation of [lazy] needs to afford the same opportunity to the optimizer. One way we could model this is as 'static' members in the behavior, perhaps.

-Joe


(Brent Royal-Gordon) #8

1. Allow behaviours to specify abstract members that need to be implemented (akin to protocols) and get the initial value of lazy from there, e.g.:

    var lazy x : T {
  func load() {
    return 33
  }
    }

  Pros: clean and clear. Cons: verbosity.

The existing proposal's accessor feature does this. (The syntax would just be `load {}`, not `func load() {}`, though.)

···

--
Brent Royal-Gordon
Architechies


(Taras Zakharko) #9

Ah, yes, sorry, should have double checked with the initial text before sending the message. For some reason I though that the accessor requirement declarations were limited to get/set. But I think that my basic point still holds. E.g. in Joe’s proposal, I don’t really see how lazy behaviour is achieved: the sample implementation for lazy he proposes seems to force evaluation of the initial value at the declaration time. Unless we have some sort of magic that wraps the initial value into a closure (but that is then the same as what Anton suggests, albeit explicitly).

···

On 14 Feb 2016, at 09:19, Brent Royal-Gordon <brent@architechies.com> wrote:

1. Allow behaviours to specify abstract members that need to be implemented (akin to protocols) and get the initial value of lazy from there, e.g.:

   var lazy x : T {
  func load() {
    return 33
  }
   }

Pros: clean and clear. Cons: verbosity.

The existing proposal's accessor feature does this. (The syntax would just be `load {}`, not `func load() {}`, though.)

--
Brent Royal-Gordon
Architechies


(Brent Royal-Gordon) #10

Ah, yes, sorry, should have double checked with the initial text before sending the message. For some reason I though that the accessor requirement declarations were limited to get/set. But I think that my basic point still holds. E.g. in Joe’s proposal, I don’t really see how lazy behaviour is achieved: the sample implementation for lazy he proposes seems to force evaluation of the initial value at the declaration time. Unless we have some sort of magic that wraps the initial value into a closure (but that is then the same as what Anton suggests, albeit explicitly).

In Joe's proposal, the initializer expression is wrapped up in a computed property. Because it's a *computed* property, rather than a *stored* property containing a closure, it doesn't take up any extra storage in the instances.

···

--
Brent Royal-Gordon
Architechies


(Taras Zakharko) #11

Ok, thanks for explaining, now I finally understand how this is supposed to work (thats what happens when one just skims through he proposal and missed important paragraphs). So in the end, it is magic that wraps the initialiser expression in a per-declaration closure. I’d rather such things be under full programmer control. Even more, what would happen in the following case?

public behavior var [custom] _: Value = initialValue {
  var value: Value = initialValue // alternatively init() { value = initialValue}
  get
  {
   return value
  }
  set
  {
    value = newValue
  }
}

var global_x = 1

class Test {
var [custom] x : Int = global_x + 1
}

let t1 = Test()
print(t1.x)
global_x = 2
let t2 = Test()
print(t2.x) // 2 or 3???

What gets printed in the last line? If I read the proposal correctly (but by now I expect to have missed something AGAIN in all honestly), the initialValue expression is gets wrapped in a computed property. This property is first accessed when the storage is initialised, i.e. when the host object instance is constructed. This means that t1 and t2 should get two different initial values which is probably not what one would have intended. Implicit wrapping of the initialiser is a good idea if we are interested in the lazy behaviour but it could be a source of bugs if we are *not*.

— Taras

···

On 14 Feb 2016, at 11:58, Brent Royal-Gordon <brent@architechies.com> wrote:

Ah, yes, sorry, should have double checked with the initial text before sending the message. For some reason I though that the accessor requirement declarations were limited to get/set. But I think that my basic point still holds. E.g. in Joe’s proposal, I don’t really see how lazy behaviour is achieved: the sample implementation for lazy he proposes seems to force evaluation of the initial value at the declaration time. Unless we have some sort of magic that wraps the initial value into a closure (but that is then the same as what Anton suggests, albeit explicitly).

In Joe's proposal, the initializer expression is wrapped up in a computed property. Because it's a *computed* property, rather than a *stored* property containing a closure, it doesn't take up any extra storage in the instances.

--
Brent Royal-Gordon
Architechies


(Brent Royal-Gordon) #12

This means that t1 and t2 should get two different initial values which is probably not what one would have intended.

On the contrary, that's how initial values always work in Swift. Try it yourself: if you copy that code sample, delete the `[custom]`, and paste it into a Swift REPL (any version, as far as I know), it will print "2" and "3".

This semantic is important in several cases, like assigning unique IDs, but it's perhaps most crucial when you're initializing with a reference type. When you write something like this:

  struct Foo {
    let bar = NSMutableArray()
  }

Each `Foo` needs to get its own, separate instance of NSMutableArray, rather than all of them sharing a single array. Reevaluating the initial value for every initialization achieves that goal.

Given that it's the way the rest of the language works, I really don't think it'll be particularly surprising if this semantic carries over to property behaviors, too.

···

--
Brent Royal-Gordon
Architechies


(Taras Zakharko) #13

You are right! Ok, I’m convinced now as far as that goes :slight_smile: Smart design. I didn’t think that far, I’m afraid.

Thanks for explaining again,

T.

···

On 14 Feb 2016, at 13:33, Brent Royal-Gordon <brent@architechies.com> wrote:

This means that t1 and t2 should get two different initial values which is probably not what one would have intended.

On the contrary, that's how initial values always work in Swift. Try it yourself: if you copy that code sample, delete the `[custom]`, and paste it into a Swift REPL (any version, as far as I know), it will print "2" and "3".

This semantic is important in several cases, like assigning unique IDs, but it's perhaps most crucial when you're initializing with a reference type. When you write something like this:

  struct Foo {
    let bar = NSMutableArray()
  }

Each `Foo` needs to get its own, separate instance of NSMutableArray, rather than all of them sharing a single array. Reevaluating the initial value for every initialization achieves that goal.

Given that it's the way the rest of the language works, I really don't think it'll be particularly surprising if this semantic carries over to property behaviors, too.

--
Brent Royal-Gordon
Architechies


(Anton Zhilin) #14

A second try at improving syntax for properties, aiming for unification and
consistency. This time I suggest to declare initialValue as an accessor, as
it is really an accessor with shorthand syntax. For this and a couple of
other ideas, please see the gist
<https://gist.github.com/Anton3/f71a3e2ee29dffe1b9b2>.

···

2016-02-14 15:41 GMT+03:00 Taras Zakharko <taras.zakharko@uzh.ch>:

You are right! Ok, I’m convinced now as far as that goes :slight_smile: Smart design.
I didn’t think that far, I’m afraid.

Thanks for explaining again,

T.

> On 14 Feb 2016, at 13:33, Brent Royal-Gordon <brent@architechies.com> > wrote:
>
>> This means that t1 and t2 should get two different initial values which
is probably not what one would have intended.
>
> On the contrary, that's how initial values always work in Swift. Try it
yourself: if you copy that code sample, delete the `[custom]`, and paste it
into a Swift REPL (any version, as far as I know), it will print "2" and
"3".
>
> This semantic is important in several cases, like assigning unique IDs,
but it's perhaps most crucial when you're initializing with a reference
type. When you write something like this:
>
> struct Foo {
> let bar = NSMutableArray()
> }
>
> Each `Foo` needs to get its own, separate instance of NSMutableArray,
rather than all of them sharing a single array. Reevaluating the initial
value for every initialization achieves that goal.
>
> Given that it's the way the rest of the language works, I really don't
think it'll be particularly surprising if this semantic carries over to
property behaviors, too.
>
> --
> Brent Royal-Gordon
> Architechies
>