Property Selectors


(Andrew Thompson) #1

Hello Swift Evolution Community,

I’ve been thinking about a new language feature that would allow properties to be first class citizens. The basic idea is as follows:

  let x: PropertySelector<UIView, CGFloat> = #property(UIView.frame.origin.x)
  let view: UIView = …
  view.frame.origin.x = 20
  x.read(view) // returns 20
  x.write(view, value: 9091)
  view.frame.origin.x // now 9091

This is a trivial example, but now we can do more interesting things in our code. For example, we can animate any property on a view (that is documented to be animatable of course):

  func animate(view: UIView, property: PropertySelector<UIView, CGFloat>, amount: Int) {
    let originalValue = property.read(view)
    func generateKeyFrames() {
      let step = 1.0 / Double(amount)
      for i in 0..<amount {
        let newValue = originalValue + CGFloat(i)
        let time = Double(i) / Double(amount)
        UIView.addKeyframe(withRelativeStartTime: time,
                  relativeDuration: step,
                  animations: { property.write(view, value: newValue) }
        )
      }
    }
    
    UIView.animateKeyframes(withDuration: 1.0,
                 delay: 0,
                 options: [],
                 animations: generateKeyFrames,
                 completion: nil)
  }

  let myView: UIView = …
  myView.frame = CGRect(x: 20, y: 100, width: 99, height: 120)

  // once this completes, myView.frame.origin.x == 120
  animate(view: myView, property: #property(UIView.frame.origin.x), amount: 100)
  
  // once this completes, myView.frame.size.width == 198
  animate(view: myView, property: #property(UIView.frame.size.width), amount: 99)

I think this would be a pretty neat feature to have, what do you think?

Cheers,
- Andrew


(David Sweeris) #2

I think you can already do that with `UnsafeMutablePointer`. I think. I'm really tired, so maybe if I look at it again in the morning I'll immediately see a difference. Apart from the syntax, of course, which is nice.

-Dave Sweeris

···

Sent from my iPhone

On Mar 14, 2017, at 01:02, Andrew Thompson via swift-evolution <swift-evolution@swift.org> wrote:

Hello Swift Evolution Community,

I’ve been thinking about a new language feature that would allow properties to be first class citizens. The basic idea is as follows:

   let x: PropertySelector<UIView, CGFloat> = #property(UIView.frame.origin.x)
   let view: UIView = …
   view.frame.origin.x = 20
   x.read(view) // returns 20
   x.write(view, value: 9091)
   view.frame.origin.x // now 9091

This is a trivial example, but now we can do more interesting things in our code. For example, we can animate any property on a view (that is documented to be animatable of course):

   func animate(view: UIView, property: PropertySelector<UIView, CGFloat>, amount: Int) {
       let originalValue = property.read(view)
       func generateKeyFrames() {
           let step = 1.0 / Double(amount)
           for i in 0..<amount {
               let newValue = originalValue + CGFloat(i)
               let time = Double(i) / Double(amount)
               UIView.addKeyframe(withRelativeStartTime: time,
                                 relativeDuration: step,
                                 animations: { property.write(view, value: newValue) }
               )
           }
       }
       
       UIView.animateKeyframes(withDuration: 1.0,
                              delay: 0,
                              options: [],
                              animations: generateKeyFrames,
                              completion: nil)
   }

   let myView: UIView = …
   myView.frame = CGRect(x: 20, y: 100, width: 99, height: 120)

   // once this completes, myView.frame.origin.x == 120
   animate(view: myView, property: #property(UIView.frame.origin.x), amount: 100)
   
   // once this completes, myView.frame.size.width == 198
   animate(view: myView, property: #property(UIView.frame.size.width), amount: 99)

I think this would be a pretty neat feature to have, what do you think?


(Jaden Geller) #3

Sent from my iPhone

Hello Swift Evolution Community,

I’ve been thinking about a new language feature that would allow properties to be first class citizens. The basic idea is as follows:

  let x: PropertySelector<UIView, CGFloat> = #property(UIView.frame.origin.x)
  let view: UIView = …
  view.frame.origin.x = 20
  x.read(view) // returns 20
  x.write(view, value: 9091)
  view.frame.origin.x // now 9091

This is a trivial example, but now we can do more interesting things in our code. For example, we can animate any property on a view (that is documented to be animatable of course):

  func animate(view: UIView, property: PropertySelector<UIView, CGFloat>, amount: Int) {
      let originalValue = property.read(view)
      func generateKeyFrames() {
          let step = 1.0 / Double(amount)
          for i in 0..<amount {
              let newValue = originalValue + CGFloat(i)
              let time = Double(i) / Double(amount)
              UIView.addKeyframe(withRelativeStartTime: time,
                                relativeDuration: step,
                                animations: { property.write(view, value: newValue) }
              )
          }
      }

      UIView.animateKeyframes(withDuration: 1.0,
                             delay: 0,
                             options: [],
                             animations: generateKeyFrames,
                             completion: nil)
  }

  let myView: UIView = …
  myView.frame = CGRect(x: 20, y: 100, width: 99, height: 120)

  // once this completes, myView.frame.origin.x == 120
  animate(view: myView, property: #property(UIView.frame.origin.x), amount: 100)

  // once this completes, myView.frame.size.width == 198
  animate(view: myView, property: #property(UIView.frame.size.width), amount: 99)

I think this would be a pretty neat feature to have, what do you think?

I think you can already do that with `UnsafeMutablePointer`. I think. I'm really tired, so maybe if I look at it again in the morning I'll immediately see a difference. Apart from the syntax, of course, which is nice.

You definitely shouldn't try to replicate this with a pointer. At least one place that will break down is computed properties (as well as `didSet` and friends), but I imagine there are more. Also, this sort of operation should not require dropping down to an unsafe construct!

It's worth noting this is sort of equivalent to defining a type that stores functions that, given a type, will get or set a certain property. It would need to be initialized with 2 lambdas though.

···

On Mar 14, 2017, at 1:42 AM, David Sweeris via swift-evolution <swift-evolution@swift.org> wrote:

On Mar 14, 2017, at 01:02, Andrew Thompson via swift-evolution <swift-evolution@swift.org> wrote:

-Dave Sweeris

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


(David Sweeris) #4

Ah, yes! That whole sleep thing really does help.

···

Sent from my iPhone

On Mar 14, 2017, at 03:51, jaden.geller@gmail.com wrote:

On Mar 14, 2017, at 1:42 AM, David Sweeris via swift-evolution <swift-evolution@swift.org> wrote:

Sent from my iPhone

On Mar 14, 2017, at 01:02, Andrew Thompson via swift-evolution <swift-evolution@swift.org> wrote:

Hello Swift Evolution Community,

I’ve been thinking about a new language feature that would allow properties to be first class citizens. The basic idea is as follows:

let x: PropertySelector<UIView, CGFloat> = #property(UIView.frame.origin.x)
let view: UIView = …
view.frame.origin.x = 20
x.read(view) // returns 20
x.write(view, value: 9091)
view.frame.origin.x // now 9091

This is a trivial example, but now we can do more interesting things in our code. For example, we can animate any property on a view (that is documented to be animatable of course):

func animate(view: UIView, property: PropertySelector<UIView, CGFloat>, amount: Int) {
     let originalValue = property.read(view)
     func generateKeyFrames() {
         let step = 1.0 / Double(amount)
         for i in 0..<amount {
             let newValue = originalValue + CGFloat(i)
             let time = Double(i) / Double(amount)
             UIView.addKeyframe(withRelativeStartTime: time,
                               relativeDuration: step,
                               animations: { property.write(view, value: newValue) }
             )
         }
     }

     UIView.animateKeyframes(withDuration: 1.0,
                            delay: 0,
                            options: [],
                            animations: generateKeyFrames,
                            completion: nil)
}

let myView: UIView = …
myView.frame = CGRect(x: 20, y: 100, width: 99, height: 120)

// once this completes, myView.frame.origin.x == 120
animate(view: myView, property: #property(UIView.frame.origin.x), amount: 100)

// once this completes, myView.frame.size.width == 198
animate(view: myView, property: #property(UIView.frame.size.width), amount: 99)

I think this would be a pretty neat feature to have, what do you think?

I think you can already do that with `UnsafeMutablePointer`. I think. I'm really tired, so maybe if I look at it again in the morning I'll immediately see a difference. Apart from the syntax, of course, which is nice.

You definitely shouldn't try to replicate this with a pointer. At least one place that will break down is computed properties (as well as `didSet` and friends), but I imagine there are more. Also, this sort of operation should not require dropping down to an unsafe construct!

It's worth noting this is sort of equivalent to defining a type that stores functions that, given a type, will get or set a certain property. It would need to be initialized with 2 lambdas though.

-Dave Sweeris

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


(Robert Widmann) #5

Lenses <https://github.com/typelift/Focus>! My only concern is that arbitrary effects can be attached to setters and getters, which can lead to surprising results when using the property reference. As a language feature, I’d be interested to see where discussion around this will lead.

~Robert Widmann

···

On Mar 14, 2017, at 4:02 AM, Andrew Thompson via swift-evolution <swift-evolution@swift.org> wrote:

Hello Swift Evolution Community,

I’ve been thinking about a new language feature that would allow properties to be first class citizens. The basic idea is as follows:

  let x: PropertySelector<UIView, CGFloat> = #property(UIView.frame.origin.x)
  let view: UIView = …
  view.frame.origin.x = 20
  x.read(view) // returns 20
  x.write(view, value: 9091)
  view.frame.origin.x // now 9091

This is a trivial example, but now we can do more interesting things in our code. For example, we can animate any property on a view (that is documented to be animatable of course):

  func animate(view: UIView, property: PropertySelector<UIView, CGFloat>, amount: Int) {
    let originalValue = property.read(view)
    func generateKeyFrames() {
      let step = 1.0 / Double(amount)
      for i in 0..<amount {
        let newValue = originalValue + CGFloat(i)
        let time = Double(i) / Double(amount)
        UIView.addKeyframe(withRelativeStartTime: time,
                  relativeDuration: step,
                  animations: { property.write(view, value: newValue) }
        )
      }
    }
    
    UIView.animateKeyframes(withDuration: 1.0,
                 delay: 0,
                 options: [],
                 animations: generateKeyFrames,
                 completion: nil)
  }

  let myView: UIView = …
  myView.frame = CGRect(x: 20, y: 100, width: 99, height: 120)

  // once this completes, myView.frame.origin.x == 120
  animate(view: myView, property: #property(UIView.frame.origin.x), amount: 100)
  
  // once this completes, myView.frame.size.width == 198
  animate(view: myView, property: #property(UIView.frame.size.width), amount: 99)

I think this would be a pretty neat feature to have, what do you think?

Cheers,
- Andrew
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Nicholas Maccharoli) #6

+1 Lenses would be awesome!

···

On Wed, Mar 15, 2017 at 1:59 AM, Robert Widmann via swift-evolution < swift-evolution@swift.org> wrote:

Lenses <https://github.com/typelift/Focus>! My only concern is that
arbitrary effects can be attached to setters and getters, which can lead to
surprising results when using the property reference. As a language
feature, I’d be interested to see where discussion around this will lead.

~Robert Widmann

On Mar 14, 2017, at 4:02 AM, Andrew Thompson via swift-evolution < > swift-evolution@swift.org> wrote:

Hello Swift Evolution Community,

I’ve been thinking about a new language feature that would allow
properties to be first class citizens. The basic idea is as follows:

let x: PropertySelector<UIView, CGFloat> = #property(UIView.frame.origin.
x)
let view: UIView = …
view.frame.origin.x = 20
x.read(view) // returns 20
x.write(view, value: 9091)
view.frame.origin.x // now 9091

This is a trivial example, but now we can do more interesting things in
our code. For example, we can animate any property on a view (that is
documented to be animatable of course):

func animate(view: UIView, property: PropertySelector<UIView, CGFloat>,
amount: Int) {
let originalValue = property.read(view)
func generateKeyFrames() {
let step = 1.0 / Double(amount)
for i in 0..<amount {
let newValue = originalValue + CGFloat(i)
let time = Double(i) / Double(amount)
UIView.addKeyframe(withRelativeStartTime: time,
relativeDuration: step,
animations: { property.write(view, value: newValue) }
)
}
}

UIView.animateKeyframes(withDuration: 1.0,
  delay: 0,
  options: [],
  animations: generateKeyFrames,
  completion: nil)
}

let myView: UIView = …
myView.frame = CGRect(x: 20, y: 100, width: 99, height: 120)

// once this completes, myView.frame.origin.x == 120
animate(view: myView, property: #property(UIView.frame.origin.x), amount:
100)

// once this completes, myView.frame.size.width == 198
animate(view: myView, property: #property(UIView.frame.size.width),
amount: 99)

I think this would be a pretty neat feature to have, what do you think?

Cheers,
- Andrew
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

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


(Joe Groff) #7

We agree! How does this sound:

https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170313/033998.html

-Joe

···

On Mar 14, 2017, at 1:02 AM, Andrew Thompson via swift-evolution <swift-evolution@swift.org> wrote:

Hello Swift Evolution Community,

I’ve been thinking about a new language feature that would allow properties to be first class citizens. The basic idea is as follows:

  let x: PropertySelector<UIView, CGFloat> = #property(UIView.frame.origin.x)
  let view: UIView = …
  view.frame.origin.x = 20
  x.read(view) // returns 20
  x.write(view, value: 9091)
  view.frame.origin.x // now 9091

This is a trivial example, but now we can do more interesting things in our code. For example, we can animate any property on a view (that is documented to be animatable of course):

  func animate(view: UIView, property: PropertySelector<UIView, CGFloat>, amount: Int) {
    let originalValue = property.read(view)
    func generateKeyFrames() {
      let step = 1.0 / Double(amount)
      for i in 0..<amount {
        let newValue = originalValue + CGFloat(i)
        let time = Double(i) / Double(amount)
        UIView.addKeyframe(withRelativeStartTime: time,
                  relativeDuration: step,
                  animations: { property.write(view, value: newValue) }
        )
      }
    }
    
    UIView.animateKeyframes(withDuration: 1.0,
                 delay: 0,
                 options: [],
                 animations: generateKeyFrames,
                 completion: nil)
  }

  let myView: UIView = …
  myView.frame = CGRect(x: 20, y: 100, width: 99, height: 120)

  // once this completes, myView.frame.origin.x == 120
  animate(view: myView, property: #property(UIView.frame.origin.x), amount: 100)
  
  // once this completes, myView.frame.size.width == 198
  animate(view: myView, property: #property(UIView.frame.size.width), amount: 99)

I think this would be a pretty neat feature to have, what do you think?


(Andrew Thompson) #8

Opps, should have sent this to the mailing list….

That sounds pretty good. :blush: I find the response from the community to this proposal to be pretty cool.

While reading some of the discussion around your proposal, I’ve seen requests where people are wanting to retrieve the list of an object’s properties. I think this is an interesting idea to explore, particularly if it was possible to generalise about all of the properties stored on an object.

For example, consider the following:

We have a protocol, JSONEncodable, and a bunch of types that conform to it, namely `Int`, `String`, and `Data`.

  protocol JSONEncodable {
    func encode() -> String
  }

  extension Int {
    func encode() -> String { … }
  }
  
  // similarly for String and Data.

A very common scenario that occurs is composing a type where all properties conform to a particular protocol, for example:

  struct Person {
    var age: Int
    var name: String
    var identifier: Data
  }

Now, we want to have `Person` conform to `JSONEncodable`, so we could just implement the encode method ourselves:

  extension Person: JSONEncodable {
    func encode() -> String {
      return age.encode() + name.encode() + identifier.encode()
    }
  }

But this seems a little bit repetitive, what we really want is for the library author to declare the following:

  For every type T whose properties conform to protocol P, we can derive a free conformance to protocol P for type T.

Applying this, we get the following code:

  extension JSONEncodable where Self.InstanceProperties: JSONEncodable {
    func encode() -> String {
      let properties = Metatype<Self>.properties
      var output = “”
      for p in properties {
        output += p.read(self).encode()
      }
      return output
    }
  }

Now all a user needs to do is say that want to conform to the protocol:

  struct Person: JSONEncodable {
    var age: Int
    var name: String
    var identifier: Data
  }

Cheers,
- Andrew

···

On 18 Mar 2017, at 6:08 am, Joe Groff <jgroff@apple.com> wrote:

On Mar 14, 2017, at 1:02 AM, Andrew Thompson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Hello Swift Evolution Community,

I’ve been thinking about a new language feature that would allow properties to be first class citizens. The basic idea is as follows:

  let x: PropertySelector<UIView, CGFloat> = #property(UIView.frame.origin.x)
  let view: UIView = …
  view.frame.origin.x = 20
  x.read(view) // returns 20
  x.write(view, value: 9091)
  view.frame.origin.x // now 9091

This is a trivial example, but now we can do more interesting things in our code. For example, we can animate any property on a view (that is documented to be animatable of course):

  func animate(view: UIView, property: PropertySelector<UIView, CGFloat>, amount: Int) {
    let originalValue = property.read(view)
    func generateKeyFrames() {
      let step = 1.0 / Double(amount)
      for i in 0..<amount {
        let newValue = originalValue + CGFloat(i)
        let time = Double(i) / Double(amount)
        UIView.addKeyframe(withRelativeStartTime: time,
                  relativeDuration: step,
                  animations: { property.write(view, value: newValue) }
        )
      }
    }
    
    UIView.animateKeyframes(withDuration: 1.0,
                 delay: 0,
                 options: [],
                 animations: generateKeyFrames,
                 completion: nil)
  }

  let myView: UIView = …
  myView.frame = CGRect(x: 20, y: 100, width: 99, height: 120)

  // once this completes, myView.frame.origin.x == 120
  animate(view: myView, property: #property(UIView.frame.origin.x), amount: 100)
  
  // once this completes, myView.frame.size.width == 198
  animate(view: myView, property: #property(UIView.frame.size.width), amount: 99)

I think this would be a pretty neat feature to have, what do you think?

We agree! How does this sound:

https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170313/033998.html

-Joe


(Joe Groff) #9

Opps, should have sent this to the mailing list….

That sounds pretty good. :blush: I find the response from the community to this proposal to be pretty cool.

While reading some of the discussion around your proposal, I’ve seen requests where people are wanting to retrieve the list of an object’s properties. I think this is an interesting idea to explore, particularly if it was possible to generalise about all of the properties stored on an object.

I agree, I think that's a natural extension of the core key path functionality. We're trying to keep the initial work small in scope to begin with, but I think it's a good foundation to start building out a better reflection model.

-Joe

···

On Mar 19, 2017, at 6:25 PM, Andrew Thompson <mrwerdo331@me.com> wrote:

For example, consider the following:

We have a protocol, JSONEncodable, and a bunch of types that conform to it, namely `Int`, `String`, and `Data`.

  protocol JSONEncodable {
    func encode() -> String
  }

  extension Int {
    func encode() -> String { … }
  }
  
  // similarly for String and Data.

A very common scenario that occurs is composing a type where all properties conform to a particular protocol, for example:

  struct Person {
    var age: Int
    var name: String
    var identifier: Data
  }

Now, we want to have `Person` conform to `JSONEncodable`, so we could just implement the encode method ourselves:

  extension Person: JSONEncodable {
    func encode() -> String {
      return age.encode() + name.encode() + identifier.encode()
    }
  }

But this seems a little bit repetitive, what we really want is for the library author to declare the following:

  For every type T whose properties conform to protocol P, we can derive a free conformance to protocol P for type T.

Applying this, we get the following code:

  extension JSONEncodable where Self.InstanceProperties: JSONEncodable {
    func encode() -> String {
      let properties = Metatype<Self>.properties
      var output = “”
      for p in properties {
        output += p.read(self).encode()
      }
      return output
    }
  }

Now all a user needs to do is say that want to conform to the protocol:

  struct Person: JSONEncodable {
    var age: Int
    var name: String
    var identifier: Data
  }

Cheers,
- Andrew

On 18 Mar 2017, at 6:08 am, Joe Groff <jgroff@apple.com <mailto:jgroff@apple.com>> wrote:

On Mar 14, 2017, at 1:02 AM, Andrew Thompson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Hello Swift Evolution Community,

I’ve been thinking about a new language feature that would allow properties to be first class citizens. The basic idea is as follows:

  let x: PropertySelector<UIView, CGFloat> = #property(UIView.frame.origin.x)
  let view: UIView = …
  view.frame.origin.x = 20
  x.read(view) // returns 20
  x.write(view, value: 9091)
  view.frame.origin.x // now 9091

This is a trivial example, but now we can do more interesting things in our code. For example, we can animate any property on a view (that is documented to be animatable of course):

  func animate(view: UIView, property: PropertySelector<UIView, CGFloat>, amount: Int) {
    let originalValue = property.read(view)
    func generateKeyFrames() {
      let step = 1.0 / Double(amount)
      for i in 0..<amount {
        let newValue = originalValue + CGFloat(i)
        let time = Double(i) / Double(amount)
        UIView.addKeyframe(withRelativeStartTime: time,
                  relativeDuration: step,
                  animations: { property.write(view, value: newValue) }
        )
      }
    }
    
    UIView.animateKeyframes(withDuration: 1.0,
                 delay: 0,
                 options: [],
                 animations: generateKeyFrames,
                 completion: nil)
  }

  let myView: UIView = …
  myView.frame = CGRect(x: 20, y: 100, width: 99, height: 120)

  // once this completes, myView.frame.origin.x == 120
  animate(view: myView, property: #property(UIView.frame.origin.x), amount: 100)
  
  // once this completes, myView.frame.size.width == 198
  animate(view: myView, property: #property(UIView.frame.size.width), amount: 99)

I think this would be a pretty neat feature to have, what do you think?

We agree! How does this sound:

https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170313/033998.html

-Joe