[Pitch] Collection Type Element Property Observers

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>

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

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

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

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,

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

@John_McCall,

Reviving this discussion... My follow-up question to this is: how would these new collection observers be any different from the current property observers like didSet and willSet? Can't we use the same mechanisms to create new specialized observers that provide convenience for just collection types?

Wow, that's quite a time-skip, but I think I can pick up the old thread of discussion.

When we create programming languages, we try to define semantics expression-by-expression as much as possible, so each part of the language fits into the broader picture in a straightforward, well-composed way. Sometimes those semantics are context-sensitive — for example, the semantics of a reference to a mutable variable depend on whether you are assigning to it, reading from it, or passing it inout — but those are still ultimately based on general rules.

Try to come up with some general rules for your proposal. What exactly causes willAdd to be called?

  list.append(4)     // I assume this is supposed to call willAdd(4).
  list += [4]        // Does this also call willAdd(4)?
  list += [4, 5, 6]  // Does this call willAdd three times?
                     // Who makes the decision to do that?
                     // Do the additions have to happen one at a time?
  list = [1, 2, 3]   // What does this do?
                     // Does it matter if the current value is [1, 2]?
  list[0] = 4        // What does this do if the current value is [1]?
                     // What if it's already [4]?
  myAppend(&list, 4) // What happens if myAppend calls append?
                     // If that triggers willAdd, how? and when?
3 Likes

I believe it would be a good idea if this design space was fully explored on a package level before we started adding any sort of language-level sugar for it.

Observable collections have lots of non-obvious design/implementation choices, and very few of these require language (or even stdlib) modifications. SE-0240 has just introduced a difference type for ordered collections; it would very much be possible to build on this to implement an observable array type that produced a stream of such differences. (For example, see this old experiment from a few years back.)

The newly accepted proposal SE-0240 may well provide a solution. You'll be able to diff your array in the willSet / didSet hooks.