musictheory / NilScript

Objective-C-style language superset of JavaScript with a tiny, simple runtime
Other
50 stars 5 forks source link

Feature: Property observers #118

Closed iccir closed 8 years ago

iccir commented 8 years ago

A very common idiom in our source base (and Obj-C source bases) is redrawing a view in response to a property change. For example:

@implementation RoundedButton

@property Number cornerRadius;

…

- (void) setCornerRadius:(Number)radius
{
    if (_cornerRadius != cornerRadius) {
        _cornerRadius = cornerRadius;
        [self setNeedsDisplay];
    }
}

@end

A lot of our code would be reduced if the compiler could generate this boilerplate. Perhaps something like:

@implementation RoundedButton
@property (didChange=setNeedsDisplay) Number cornerRadius;
@end
iccir commented 8 years ago

I propose the following new property attribute names: willSet, didSet, willChange, didChange

willSet and didSet are inspired by Swift's property observers. They will always be called, even if a property value is the same.

willChange and didChange are only called when a property value will/did change (Using == or === for primitive values and -[BaseObject isEqual:] for objects).

iccir commented 8 years ago

Each property attribute will specify a method which will be called upon the object.

If the method signature has an argument, that argument will be the new value of the property for willSet and willChange and the old value of the property for didSet and didChange.

An extreme example:

@property (willSet=_willSetBar:,willChange=_willChangeBar,didChange=_didChangeBar:,didSet=_didSetBar) Number bar;

// Generated code
- (void) setBar:(Number)newBar
{
    let oldBar = _bar;

    [self _willSetBar:newBar];

    if (oldBar != newBar) { // Or !== ?
        [self _willChangeBar];
        _bar = newBar;
        [self _didChangeBar:oldBar];
    }

    [self _didSetBar];
}
iccir commented 8 years ago

Based on our usage, we should just use == or === for testing equality. Likely ===, as that is what lodash uses for _.isEqual.

iccir commented 8 years ago

A few things I don't like about this:

1) The use of camelCase when other property attributes use snake_case. snake_case would be more consistent, but I really don't want to type out did_change each time.

2) Having four new attributes for [will/did][Set/Change] is overkill.

Perhaps an a simpler solution would be:

@property (observer=setNeedsDisplay) Number cornerRadius;
iccir commented 8 years ago

Another issue: @property should be about the external interface to a property. It's weird to have it reference a private method. For example, we have a lot of cases where an -_updateLayer method needs to be called in response to a property change.

@property (observer=_updateLayer) Number cornerRadius;

From an Obj-C perspective, @property would be in the header file, and knowledge about _updateLayer shouldn't be present there.

It might make sense to use a separate @observe directive, in the spirit of @synthesize or @dynamic.

iccir commented 8 years ago

The syntax would likely be: @observe (ATTRIBUTES) PROPERTIES;

With ATTRIBUTES being something like:

A complex example might be:

@property Number foo;
@property Number bar;
@observe (change, before=_fooWillChange, after=_fooDidChange) foo;
@observe (set, before=_barWillSet, after=_barDidSet) bar;
@observe (change, after=setNeedsDisplay) bar, foo;

// A change to foo will call: _fooWillChange, _fooDidChange, setNeedsDisplay on self
// A change to bar will call setNeedsDisplay
// Any invocation to setBar: will also call _barWillSet and _barDidSet

While the most common scenario:

@property Number cornerRadius;
@observe (after=setNeedsDisplay) cornerRadius;

Really, only the change and after attributes would need to be implemented for the most common case.

iccir commented 8 years ago

For now, support set, change, before=, and after= attributes. Others could be added in the future as necessary. This is in the 2.0 branch and documentation has been updated.