[swift-evolution] [Proposal] Property behaviors

plx plxswift at icloud.com
Thu Jan 14 13:33:16 CST 2016


Thanks for the earlier feedback (you did mention how to resolve reference ambiguity).

After re-reading it this is a really nice proposal overall. Even in it’s current state it is a huge upgrade, and could lead to a huge productivity / implementation-quality upgrade in application code.

I do have a bunch of questions, though.

## Syntax: “In the Large”

One thing that does worry me is readability in real-world variable declarations; I agree that `@behavior(lazy)` is clunky in the small, but in the large the current syntax is questionable:

class SomeView : UIView {
  
  @IBInspectable
   public internal(set) weak var [mainThread,resettable,logged,changeObserved] contentAreaInsets: UIEdgeInsets = UIEdgeInsetsZero {
    logName { “contentAreaInsets” } // <- see note later, btw
    didChange { setNeedsUpdateConstraints() }
  }

}

…which is admittedly an extreme example, but it illustrates the point.

The below is definitely worse in the small, but feels better in the large:

class SomeView : UIView {
  
  @IBInspectable
  @behavior(mainThread,resettable,logged,changeObserved) 
  public internal(set) weak var contentAreaInsets: UIEdgeInsets = UIEdgeInsetsZero {
    logName { “contentAreaInsets” } // <- see note later, btw
    didChange { setNeedsUpdateConstraints() }
  }

}

…(I could add line breaks in the first one, but a line break anywhere between the `var` and its the name seems odd).

To be clear, I am not suggesting the above syntax as-is; I am more trying to make sure some consideration is “how does this look with longer behavior lists”, since all the examples in the proposal are for “nice” cases.

Putting the behavior-list after `var` but before the name and type seems unfortunate once you want to add a line-break in there.

## Request: Declared-Property-Name

If possible, a property analogous to `self` / `parent` / etc. getting at a behavior’s concrete “declared-name” would be amazing, although I know it’s not quite that easy, either (see @objc questions later).

## Semantics: Overrides + Behaviors

I think this is probably specified somewhere and I just don’t understand it, but I need to ask: how do overrides work for things like the following example:

class BaseView : UIView {

  var [changeObserved] text: String {
    didChange { setNeedsLayout() }
  }

}

class RefinedView : BaseView {

  // is this right? (recall property is already `changeObserved` in parent):
  override var text: String {
    didChange { invalidateIntrinsicContentSize() } 
    // ^ does parent’s didChange get called also? can I control that?
    // ^ when is this override called? when is the parent called (if it is)?
  }

  // or is this right? (needing to add another `changeObserved` here?)
  override var [changeObserved] text: String {
    didChange { invalidateIntrinsicContentSize() }
    // ^ does parent’s didChange get called also? can I control that?
    // ^ when is this override called? when is the parent called (if it is)?
  }

}

…I’m really not sure which of the above is more reasonable. 

The first variant would seem to have confusing timing (when do the calls happen relative to each other and to other behaviors)?

The other one seems to introduce a new, identically-named behavior, which seems like it’d lead to ambiguity if you had to use behavior methods/behavior properties.

## Semantics: Redundancy/“Static” Parameterization

This is an extended example, but it sort-of has to be to illustrate the concern.

Suppose we wanted to define a bunch of behaviors useful for use on UIView. I’ll provide a few examples, including just the `set` logic to keep it as short as possible:

// goal: `redraw` automatically calls `setNeedsDisplay()` when necessary:
var behavior redraw<Value:Equatable where Self:UIView> : Value {
  set { 
    if newValue != value {
      value = newValue
      self.setNeedsDisplay()
    }
  }
}

// goal: `invalidateSize` automatically calls `invalidateIntrinsicContentSize()` when necessary:
var behavior invalidateSize<Value:Equatable where Self:UIView> : Value {
  set { 
    if newValue != value {
      value = newValue
      self.invalidateIntrinsicContentSize()
    }
  }
}

…(and you can consider also `relayout`, `updateConstraints`, `updateFocus`, accessibility utilities, and so on…).

With all those in hand, we arrive at something IMHO really nice and self-documenting:

class CustomDrawnView : UIView {

  // pure-redrawing:
  var [redraw] strokeWidth: CGFloat
  var [redraw] outlineWidth: CGFloat
  var [redraw] strokeColor: UIColor
  var [redraw] outlineColor: UIColor

  // also size-impacting:
  var [redraw, invalidateSize] iconPath: UIBezierPath
  var [redraw, invalidateSize] captionText: String
  var [redraw, invalidateSize] verticalSpace: CGFloat
  
}

…but if you “expand” what happens within these behaviors, once you have multiple such behaviors in a chain (e.g. `[redraw, invalidateSize]`) you will of course have one `!=` comparison per behavior. Note that although, in this case, `!=` is hopefully not too expensive, you can also consider it as a proxy here for other, possibly-expensive operations.

On the one hand, it seems like it ought to be possible to do better here — e.g., do a single such check, not one per behavior — but on the other hand, it seems hard to augment the proposal to make it possible w/out also making it much more complex than it already is.

EG: the best hope from a readability standpoint might be something like this behavior:

var behavior invalidate<Value:Equatable where Self:UIView> {
  // `parameter` here is new syntax; explanation below
  parameter display: Bool = false
  parameter intrinsicSize: Bool = false
  
  // as-before:
  var value: Value

  // `get` omitted: 
  set {
    if newValue != value {
      value = newValue
      if display { self.setNeedsDisplay() }
      if intrinsicSize { self.invalidateIntrinsicContentSize() }
      // also imagine constraints, layout, etc.
    }
  }
}

…but to achieve that “omnibus” capability you’d need a lot of flags, each of which:

- needs to get set somehow (without terrible syntax)
- needs to get “stored" somehow (without bloating the behaviors, if possible)

Syntax to set the flags seems awkward at best:

// this seems close to ideal for such parameters:
var [invalidate(display,intrinsicSize)] iconPath: UIBezierPath

// but this seems the best-achievable option w/out dedicated compiler magic:
var [invalidate(display=true, intrinsicSize=true)] iconPath: UIBezierPath

…and at least to my eyes that "best-achievable syntax" isn’t all that great, anymore.

Likewise you’d need some way to actually store those parameters, presumably *not* as ordinary stored fields — that’s going to bloat the behaviors! — but as some new thing, whence the new `parameter` keyword.

Between that and the naming/parameter-passing, it feels like a big ask, probably too big.

FWIW, for sake of comparison, this seems to be about the best you can do under the current proposal:

class CustomDrawnView : UIView {

  // pure-redrawing:
  var [changeObserved] strokeWidth: CGFloat {
    didChange { invalidate(.Display) }
  }

  var [changeObserved] outlineWidth: CGFloat {
    didChange { invalidate(.Display) }
  }

  var [changeObserved] strokeColor: UIColor {
    didChange { invalidate(.Display) }
  }

  var [changeObserved] outlineColor: UIColor {
    didChange { invalidate(.Display) }
  }

  // also size-impacting:
  var [changeObserved] iconPath: UIBezierPath {
    didChange { invalidate([.Display, .IntrinsicContentSize]) }
  }

  var [changeObserved] captionText: String {
    didChange { invalidate([.Display, .IntrinsicContentSize]) }
  }

  var [changeObserved] verticalSpace: CGFloat {
    didChange { invalidate([.Display, .IntrinsicContentSize]) }
  }
  
}

…where `invalidate` is taking some bitmask/option-set and then calling the appropriate view methods.

This isn’t terrible, it’s just nowhere near what it might be under this proposal.

I also think it’s perfectly reasonable to see the above and decide the likely complexity of a solution probably outweighs whatever gains it might bring; I’m just bringing it up in hopes there might be an easy way to have most of the cake and also eat most of the cake.

## ObjC Interaction

One thing I am not sure about is how this interacts with @objc annotations. 

First, my assumption is that, as today, property behaviors and @objc-visibilty are essentially orthogonal (don’t really impact each other). This doesn’t seem to be stated explicitly anywhere, and it would be preserving the status quo, but it’s worth confirming just to be sure.

Secondly, right now one of the language’s minor warts is you can’t really get proper objective-c property names on some read-write properties without some doing.

You can either do this:

class Foo: NSObject {
  @objc(isPermitted)
  var permitted: Bool
}

…which gets you `isPermitted` (good) and `setIsPermitted:` (not ideal), or you can do this:

class Foo: NSObject {
  
  @nonobjc // maximize chances of efficiency
  private final var _permitted: Bool
  
  var permitted: Bool {
    @objc(isPermitted) get { return _permitted }
    @objc(setPermitted:) set { _permitted = newValue }
  }

}

…which gets the right Objective-C names but is quite clunky.

What you can’t do is this:

class Foo: NSObject {
  var permitted: Bool {
   @objc(isPermitted) get, // just rename
   @objc(setPermitted:) set // just rename 
}

…at least not to my knowledge; if there’s a trick I don’t know it.

On the one hand, this proposal doesn’t seem to change this situation.

On the other hand, if it can be changed, this seems like a reasonable time/place to do it.

That’s it for the moment.

With this proposal it seems like a really nice feature to have.

> On Jan 13, 2016, at 8:04 PM, Joe Groff <jgroff at apple.com> wrote:
> 
> 
>> On Jan 13, 2016, at 5:12 PM, plx via swift-evolution <swift-evolution at swift.org> wrote:
>> 
>> Quick Q1: is it the case that `var behavior redrawing<Value where Self:UIView> : Value { … } ` would work or no? I’d assume so, but I don’t think there are any examples with a non-protocol constraint on `Self`, making it hard to tell at a glance.
> 
> Yeah, you should be able to use arbitrary generic constraints.
> 
>> 
>> Quick Q2: is there anything you can do in this scenario:
>> 
>> // w/in some behavior:
>> mutating func silentlySet(value: Value) {
>> value = value // <- probably not going to work
>> self.value = value // <- not right either, `self` is the behavior’s owner, right?
>> }
>> 
>> …other than change the argument name to avoid conflict?
> 
> I thought I mentioned this in the proposal—you could use `behaviorName.value` to qualify a reference to the behavior's members within the behavior.
> 
>> 
>> Remark: it definitely feels a bit odd to be using both `Self` and `self` to mean something that’s neither consistent with the rest of the language nor, really, to mean `Self` (or `self`). 
>> 
>> I get not wanting new keywords, but this feels like it could be an economy too far; perhaps I’m misunderstanding some aspect of how it’s meant to work.
> 
> I'm not totally comfortable with it either. It at least corresponds to the notion of `self` you'd get if you'd coded a property by hand within its enclosing type, so the meaning might be useful for refactoring code out of concrete property implementations into behavior templates.
> 
> -Joe



More information about the swift-evolution mailing list