[Pitch] Collection Type Element Property Observers


(Sean Alling) #1

PROPOSAL:

I propose the addition of the following new property observers applicable to all Collection types (Dictionary, Set, and Array):

– willAdd(newValue) { … }
– didAdd(newValue) { … }

– willRemove(oldValue) { … }
– didRemove(oldValue) { … }

where, `newValue` and `oldValue` are immutable.

This would allow one to perform additional work or observer logic to values added and removed from collections. This model is consistent with Swift syntax and may perhaps minimize the use of NSNotifications in some situations.

Currently, in order to perform this functionality some filtering and comparison logic would have to be performed from within a `willSet { … }` and `didSet { …}` call. This change would not only ease that burden but promote a more visible and explicit expression that can further improve readability and traceability of functionality.

EXAMPLE USAGE:

var list = [objects]() {
  willAdd(newValue) {
    …
  }
  didAdd(newValue) {
    …
  }
}

var list = [key : object]() {
  willRemove(oldValue) {
    …
  }
  didRemove(oldValue) {
    …
  }
}

···

-----
Sean Alling
allings@icloud.com <mailto:allings@icloud.com>


(Joshua Alvarado) #2

I believe this will this impact performance on collections if you have to
do a computation on every item.

Also what about appending another collection to an existing collection?
Will there be a willAdd(newValue: Collection)?
Or will the willAdd(element: Element) be called multiple times?

I think think the gain of functionality isn't there for the addition of
this functionality to be added. There are other ways to implement what you
are desiring to do without adding it to Swift language.

···

On Thu, Mar 30, 2017 at 12:37 PM, Sean Alling via swift-evolution < swift-evolution@swift.org> wrote:

*PROPOSAL:*

I propose the addition of the following new property observers applicable
to all Collection types (Dictionary, Set, and Array):

– *willAdd(newValue) { … }*
– *didAdd(newValue) { … }*

– *willRemove(oldValue) { … }*
– *didRemove(oldValue) { … }*

where, `newValue` and `oldValue` are *immutable*.

This would allow one to perform additional work or observer logic to
values added and removed from collections. This model is consistent with
Swift syntax and may perhaps minimize the use of NSNotifications in some
situations.

Currently, in order to perform this functionality some filtering and
comparison logic would have to be performed from within a `willSet { … }`
and `didSet { …}` call. This change would not only ease that burden but
promote a more visible and explicit expression that can further improve
readability and traceability of functionality.

*EXAMPLE USAGE:*

var list = [objects]() {
willAdd(newValue) {

}
didAdd(newValue) {

}
}

var list = [key : object]() {
willRemove(oldValue) {

}
didRemove(oldValue) {

}
}

-----
Sean Alling
allings@icloud.com

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

--
Joshua Alvarado
alvaradojoshua0@gmail.com


(John McCall) #3

PROPOSAL:

I propose the addition of the following new property observers applicable to all Collection types (Dictionary, Set, and Array):

– willAdd(newValue) { … }
– didAdd(newValue) { … }

– willRemove(oldValue) { … }
– didRemove(oldValue) { … }

where, `newValue` and `oldValue` are immutable.

This would allow one to perform additional work or observer logic to values added and removed from collections. This model is consistent with Swift syntax and may perhaps minimize the use of NSNotifications in some situations.

Currently, in order to perform this functionality some filtering and comparison logic would have to be performed from within a `willSet { … }` and `didSet { …}` call. This change would not only ease that burden but promote a more visible and explicit expression that can further improve readability and traceability of functionality.

Figuring out that an arbitrary change to a collection is an "add" or a "remove" of a specific element is, well, let's just say it's complex. If you're imagining that these observers would just get magically called when someone called the add or remove method on the property, that's not really how these language features work together.

The property behaviors proposal would let you do things like automatically computing differences and calling these observers, if you really want to do that. But the better solution is almost certainly to (1) make the collection property private(set) and (2) just declare addToList and removeFromList methods that do whatever you would want to do in the observer.

John.

···

On Mar 30, 2017, at 2:37 PM, Sean Alling via swift-evolution <swift-evolution@swift.org> wrote:

EXAMPLE USAGE:

var list = [objects]() {
  willAdd(newValue) {
    …
  }
  didAdd(newValue) {
    …
  }
}

var list = [key : object]() {
  willRemove(oldValue) {
    …
  }
  didRemove(oldValue) {
    …
  }
}

-----
Sean Alling
allings@icloud.com <mailto:allings@icloud.com>_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Brent Royal-Gordon) #4

For instance, wrap your type in something like this:

struct Recorded<Base: RangeReplaceableCollection> {
    init(_ base: Base) {
        self.base = base
        changes = [ Change(newElements: base) ]
    }
    
    fileprivate(set) var base: Base
    fileprivate(set) var changes: [Change]
}

extension Recorded {
    final class Change: Hashable, CustomStringConvertible {
        let subrange: Range<Recorded.Index>?
        let newElements: [Recorded.Iterator.Element]
        
        init<C: Collection>(subrange: Range<Index>? = nil, newElements: C)
            where C.Iterator.Element == Recorded.Iterator.Element
        {
            self.subrange = subrange
            self.newElements = Array(newElements)
        }
        
        static func == (lhs: Change, rhs: Change) -> Bool {
            return lhs === rhs
        }
        
        var hashValue: Int {
            return ObjectIdentifier(self).hashValue
        }
        
        var description: String {
            if let subrange = subrange {
                return "base[\(subrange.description)] = \(newElements.description)"
            }
            else {
                return "base = \(newElements.description)"
            }
        }
        
        func apply(to c: inout Recorded) {
            let subrange = self.subrange ?? c.startIndex ..< c.endIndex
            c.base.replaceSubrange(subrange, with: newElements)
            c.changes.append(self)
        }
    }
    
    mutating func apply(_ changes: [Change]) {
        for change in changes {
            change.apply(to: &self)
        }
    }
    
    func newChanges(since older: Recorded) -> [Change] {
        var changes = self.changes
        
        guard let lastChange = older.changes.last,
               let i = changes.index(of: lastChange) else {
            return changes
        }
        
        let overlapRange = 0 ... i
        precondition(
            older.changes.suffix(overlapRange.count) == changes[overlapRange],
            "self includes old changes not present in older"
        )
        
        changes.removeSubrange(0 ... i)
        return changes
    }
}

extension Recorded: RangeReplaceableCollection {
    subscript(_ i: Base.Index) -> Base.Iterator.Element {
        get {
            return base[i]
        }
        set {
            replaceSubrange(i ..< index(after: i), with: [newValue])
        }
    }
    
    func index(after i: Base.Index) -> Base.Index {
        return base.index(after: i)
    }
    
    var startIndex: Base.Index {
        return base.startIndex
    }
    
    var endIndex: Base.Index {
        return base.endIndex
    }
    
    init() {
        self.init(Base())
    }
    
    mutating func replaceSubrange<C>(_ subrange: Range<Base.Index>, with newElements: C)
        where C : Collection,
        C.Iterator.Element == Base.Iterator.Element
    {
        let change = Change(subrange: subrange, newElements: newElements)
        change.apply(to: &self)
    }
}

(This is begging for Swift 4's conditional conformance feature, which would allow `Recorded` to conform to `RandomAccessCollection` et.al. if the underlying type did.)

Now you have a ready-made change list, and all you need to do is write a `didSet` that runs `newChanges(since: oldValue)` on the new value and figures out what to do with it. That ought to be faster than a full difference calculation from scratch on every `didSet`.

···

On Mar 30, 2017, at 1:20 PM, Joshua Alvarado via swift-evolution <swift-evolution@swift.org> wrote:

I think think the gain of functionality isn't there for the addition of this functionality to be added. There are other ways to implement what you are desiring to do without adding it to Swift language.

--
Brent Royal-Gordon
Architechies


(Sean Alling) #5

John,

Sure, that is the pattern most commonly adopted for these cases but it does in fact create a considerable amount of boilerplate code. My hope was to reduce the amount of boilerplate and the burden to create such a common pattern of functionality.

I do understand that the implementation would be complex. I am imagining that these observers would get magically called when someone adds or removes the collection. The wrinkle I think you’re referring to is the fact that there are a variety of ways in which a collection can be 'added to' and 'removed from’. Is that the complexity you are referring to?

Sean

···

On Mar 30, 2017, at 2:58 PM, John McCall <rjmccall@apple.com> wrote:

On Mar 30, 2017, at 2:37 PM, Sean Alling via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
PROPOSAL:

I propose the addition of the following new property observers applicable to all Collection types (Dictionary, Set, and Array):

– willAdd(newValue) { … }
– didAdd(newValue) { … }

– willRemove(oldValue) { … }
– didRemove(oldValue) { … }

where, `newValue` and `oldValue` are immutable.

This would allow one to perform additional work or observer logic to values added and removed from collections. This model is consistent with Swift syntax and may perhaps minimize the use of NSNotifications in some situations.

Currently, in order to perform this functionality some filtering and comparison logic would have to be performed from within a `willSet { … }` and `didSet { …}` call. This change would not only ease that burden but promote a more visible and explicit expression that can further improve readability and traceability of functionality.

Figuring out that an arbitrary change to a collection is an "add" or a "remove" of a specific element is, well, let's just say it's complex. If you're imagining that these observers would just get magically called when someone called the add or remove method on the property, that's not really how these language features work together.

The property behaviors proposal would let you do things like automatically computing differences and calling these observers, if you really want to do that. But the better solution is almost certainly to (1) make the collection property private(set) and (2) just declare addToList and removeFromList methods that do whatever you would want to do in the observer.

John.

EXAMPLE USAGE:

var list = [objects]() {
  willAdd(newValue) {
    …
  }
  didAdd(newValue) {
    …
  }
}

var list = [key : object]() {
  willRemove(oldValue) {
    …
  }
  didRemove(oldValue) {
    …
  }
}

-----
Sean Alling
allings@icloud.com <mailto:allings@icloud.com>_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(John McCall) #6

John,

Sure, that is the pattern most commonly adopted for these cases but it does in fact create a considerable amount of boilerplate code. My hope was to reduce the amount of boilerplate and the burden to create such a common pattern of functionality.

I do understand that the implementation would be complex. I am imagining that these observers would get magically called when someone adds or removes the collection. The wrinkle I think you’re referring to is the fact that there are a variety of ways in which a collection can be 'added to' and 'removed from’. Is that the complexity you are referring to?

Yes. For example, you could pass the collection as an inout argument to a function that does who-knows-what to it.

John.

···

On Mar 30, 2017, at 3:16 PM, Sean Alling <allings@icloud.com> wrote:

Sean

On Mar 30, 2017, at 2:58 PM, John McCall <rjmccall@apple.com <mailto:rjmccall@apple.com>> wrote:

On Mar 30, 2017, at 2:37 PM, Sean Alling via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
PROPOSAL:

I propose the addition of the following new property observers applicable to all Collection types (Dictionary, Set, and Array):

– willAdd(newValue) { … }
– didAdd(newValue) { … }

– willRemove(oldValue) { … }
– didRemove(oldValue) { … }

where, `newValue` and `oldValue` are immutable.

This would allow one to perform additional work or observer logic to values added and removed from collections. This model is consistent with Swift syntax and may perhaps minimize the use of NSNotifications in some situations.

Currently, in order to perform this functionality some filtering and comparison logic would have to be performed from within a `willSet { … }` and `didSet { …}` call. This change would not only ease that burden but promote a more visible and explicit expression that can further improve readability and traceability of functionality.

Figuring out that an arbitrary change to a collection is an "add" or a "remove" of a specific element is, well, let's just say it's complex. If you're imagining that these observers would just get magically called when someone called the add or remove method on the property, that's not really how these language features work together.

The property behaviors proposal would let you do things like automatically computing differences and calling these observers, if you really want to do that. But the better solution is almost certainly to (1) make the collection property private(set) and (2) just declare addToList and removeFromList methods that do whatever you would want to do in the observer.

John.

EXAMPLE USAGE:

var list = [objects]() {
  willAdd(newValue) {
    …
  }
  didAdd(newValue) {
    …
  }
}

var list = [key : object]() {
  willRemove(oldValue) {
    …
  }
  didRemove(oldValue) {
    …
  }
}

-----
Sean Alling
allings@icloud.com <mailto:allings@icloud.com>_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution