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.
Good point. Longer behavior compositions are something to consider. If behaviors have dedicated declarations instead of being applications of plain types or functions, it also becomes more reasonable to let them be used as attributes by themselves:
@mainThread @resettable @logged @changeObserved
public var foo: Int { ... }
I shied away from that design originally because it would be problematic to pollute the attribute namespace with potentially every function and/or type. That's less of a problem with this design.
## 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).
That's definitely an interesting extension to consider. I think it can be separated from the core proposal, though.
## 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.
I would expect each override's [changeObserved] behavior to wrap the previous override, as happens if you override with `didSet`/`willSet` today. You raise the interesting question of what happens with the behavior names, since you do end up with two same-named behaviors applied at different times. That potentially requires deeper qualification by type; if we use Tal's suggested `.[behavior]` qualification syntax, you could refer to `.[BaseView.changeObserved]` and `.[RefinedView.changeObserved]` if you needed to.
## 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.
It seems to me you could factor at least some of this boilerplate into a behavior, reducing it to:
var [invalidate] foo: String { invalidates { return [.Display, .ContentSize] } }
Brent mentioned in the other thread about modeling `lazy` with an accessor some possibilities to whittle this down further, by allowing for implicit single-expression return in accessors, and inferring behaviors from accessor names:
var foo: String { invalidates { [.Display, .ContentSize] } }
+1 to allowing implicit single-expression return. Is there a reason this isn’t possible everywhere we can return a value? It seems to me like that is an orthogonal issue and a proposal to allow it everywhere might be a good idea.