How to implement a Mutable Ordered Set generic type formerly known as NSMutableOrderedSet in native Swift?

后端 未结 2 1488
攒了一身酷
攒了一身酷 2020-12-10 00:25

I am trying to implement a generic Mutable Ordered Set type and it needs to conform to many protocols to behave the same way as an Array and a Set does in Swift. First of al

相关标签:
2条回答
  • 2020-12-10 01:00

    A native Swift implementation of a Mutable Ordered Set:

    public struct OrderedSet<Element: Hashable> {
        public init() { }
        private var elements: [Element] = []
        private var set: Set<Element> = []
    }
    

    Conforming to the MutableCollection Protocol

    To add conformance to the MutableCollection protocol to your own custom collection, upgrade your type’s subscript to support both read and write access. A value stored into a subscript of a MutableCollection instance must subsequently be accessible at that same position. That is, for a mutable collection instance a, index i, and value x, the two sets of assignments in the following code sample must be equivalent:

    extension OrderedSet: MutableCollection {
        public subscript(index: Index) -> Element {
            get { elements[index] }
            set {
                guard let newMember = set.update(with: newValue) else { return }
                elements[index] = newMember
            }
        }
    }
    

    Conforming to the RandomAccessCollection Protocol

    The RandomAccessCollection protocol adds further constraints on the associated Indices and SubSequence types, but otherwise imposes no additional requirements over the BidirectionalCollection protocol. However, in order to meet the complexity guarantees of a random-access collection, either the index for your custom type must conform to the Strideable protocol or you must implement the index(_:offsetBy:) and distance(from:to:) methods with O(1) efficiency.

    extension OrderedSet: RandomAccessCollection {
        
        public typealias Index = Int
        public typealias Indices = Range<Int>
        
        public typealias SubSequence = Slice<OrderedSet<Element>>
        public typealias Iterator = IndexingIterator<Self>
        
        // Generic subscript to support `PartialRangeThrough`, `PartialRangeUpTo`, `PartialRangeFrom` and `FullRange` 
        public subscript<R: RangeExpression>(range: R) -> SubSequence where Index == R.Bound { .init(elements[range]) }
        
        public var endIndex: Index { elements.endIndex }
        public var startIndex: Index { elements.startIndex }
    
        public func formIndex(after i: inout Index) { elements.formIndex(after: &i) }
        
        public var isEmpty: Bool { elements.isEmpty }
    
        @discardableResult
        public mutating func append(_ newElement: Element) -> Bool { insert(newElement).inserted }
    }
    

    Conforming to the Hashable Protocol

    To use your own custom type in a set or as the key type of a dictionary, add Hashable conformance to your type. The Hashable protocol inherits from the Equatable protocol, so you must also satisfy that protocol’s requirements. The compiler automatically synthesizes your custom type’s Hashable and requirements when you declare Hashable conformance in the type’s original declaration and your type meets these criteria: For a struct, all its stored properties must conform to Hashable. For an enum, all its associated values must conform to Hashable. (An enum without associated values has Hashable conformance even without the declaration.)

    extension OrderedSet: Hashable {
        public static func ==(lhs: Self, rhs: Self) -> Bool { lhs.elements.elementsEqual(rhs.elements) }
    }
    

    Conforming to the SetAlgebra Protocol

    When implementing a custom type that conforms to the SetAlgebra protocol, you must implement the required initializers and methods. For the inherited methods to work properly, conforming types must meet the following axioms. Assume that S is a custom type that conforms to the SetAlgebra protocol, x and y are instances of S, and e is of type S.Element—the type that the set holds.

    S() == [ ]

    x.intersection(x) == x

    x.intersection([ ]) == [ ]

    x.union(x) == x

    x.union([ ]) == x x.contains(e) implies x.union(y).contains(e)

    x.union(y).contains(e) implies x.contains(e) || y.contains(e)

    x.contains(e) && y.contains(e) if and only if x.intersection(y).contains(e)

    x.isSubset(of: y) implies x.union(y) == y

    x.isSuperset(of: y) implies x.union(y) == x

    x.isSubset(of: y) if and only if y.isSuperset(of: x)

    x.isStrictSuperset(of: y) if and only if x.isSuperset(of: y) && x != y

    x.isStrictSubset(of: y) if and only if x.isSubset(of: y) && x != y

    extension OrderedSet: SetAlgebra {
        public mutating func insert(_ newMember: Element) -> (inserted: Bool, memberAfterInsert: Element) {
            let insertion = set.insert(newMember)
            if insertion.inserted {
                elements.append(newMember)
            }
            return insertion
        }
        public mutating func remove(_ member: Element) -> Element? {
            if let index = elements.firstIndex(of: member) {
                elements.remove(at: index)
                return set.remove(member)
            }
            return nil
        }
        public mutating func update(with newMember: Element) -> Element? {
            if let index = elements.firstIndex(of: newMember) {
                elements[index] = newMember
                return set.update(with: newMember)
            } else {
                elements.append(newMember)
                set.insert(newMember)
                return nil
            }
        }
        
        public func union(_ other: Self) -> Self {
            var orderedSet = self
            orderedSet.formUnion(other)
            return orderedSet
        }
        public func intersection(_ other: Self) -> Self {
            var orderedSet = self
            orderedSet.formIntersection(other)
            return orderedSet
        }
        public func symmetricDifference(_ other: Self) -> Self {
            var orderedSet = self
            orderedSet.formSymmetricDifference(other)
            return orderedSet
        }
        
        public mutating func formUnion(_ other: Self) {
            other.forEach { append($0) }
        }
        public mutating func formIntersection(_ other: Self) {
            self = .init(filter { other.contains($0) })
        }
        public mutating func formSymmetricDifference(_ other: Self) {
            self = .init(filter { !other.set.contains($0) } + other.filter { !set.contains($0) })
        }
    }
    

    Conforming to ExpressibleByArrayLiteral

    Add the capability to be initialized with an array literal to your own custom types by declaring an init(arrayLiteral:) initializer. The following example shows the array literal initializer for a hypothetical OrderedSet type, which has setlike semantics but maintains the order of its elements.

    extension OrderedSet: ExpressibleByArrayLiteral {
        public init(arrayLiteral: Element...) {
            self.init()
            for element in arrayLiteral {
                self.append(element)
            }
        }
    }
    

    extension OrderedSet: CustomStringConvertible {
        public var description: String { .init(describing: elements) }
    }
    

    Conforming to the AdditiveArithmetic Protocol

    To add AdditiveArithmetic protocol conformance to your own custom type, implement the required operators, and provide a static zero property using a type that can represent the magnitude of any value of your custom type.

    extension OrderedSet: AdditiveArithmetic {
        public static var zero: Self { .init() }
        public static func + (lhs: Self, rhs: Self) -> Self { lhs.union(rhs) }
        public static func - (lhs: Self, rhs: Self) -> Self { lhs.subtracting(rhs) }
        public static func += (lhs: inout Self, rhs: Self) { lhs.formUnion(rhs) }
        public static func -= (lhs: inout Self, rhs: Self) { lhs.subtract(rhs) }
    }
    

    Conforming to the RangeReplaceableCollection Protocol

    To add RangeReplaceableCollection conformance to your custom collection, add an empty initializer and the replaceSubrange(:with:) method to your custom type. RangeReplaceableCollection provides default implementations of all its other methods using this initializer and method. For example, the removeSubrange(:) method is implemented by calling replaceSubrange(_:with:) with an empty collection for the newElements parameter. You can override any of the protocol’s required methods to provide your own custom implementation.

    extension OrderedSet: RangeReplaceableCollection {
    
        public init<S>(_ elements: S) where S: Sequence, S.Element == Element {
            elements.forEach { set.insert($0).inserted ? self.elements.append($0) : () }
        }
            
        mutating public func replaceSubrange<C: Collection, R: RangeExpression>(_ subrange: R, with newElements: C) where Element == C.Element, C.Element: Hashable, Index == R.Bound {
            elements[subrange].forEach { set.remove($0) }
            elements.removeSubrange(subrange)
            newElements.forEach { set.insert($0).inserted ? elements.append($0) : () }
        }
    }
    

    OrderedSet Playground Sample

    Quick Playground Test (OrderedSet should have all methods that are available to Swift native Array and Set structures)

    var ordereSet1: OrderedSet = [1,2,3,4,5,6,1,2,3]  // [1, 2, 3, 4, 5, 6]
    var ordereSet2: OrderedSet = [4,5,6,7,8,9,7,8,9]  // [4, 5, 6, 7, 8, 9]
    
    ordereSet1 == ordereSet2                     // false
    ordereSet1.union(ordereSet2)                 // [1, 2, 3, 4, 5, 6, 7, 8, 9]
    
    ordereSet1.intersection(ordereSet2)          // [4, 5, 6]
    ordereSet1.symmetricDifference(ordereSet2)   // [1, 2, 3, 7, 8, 9]
    
    ordereSet1.subtract(ordereSet2)              // [1, 2, 3]
    ordereSet2.popLast()                         // 9
    
    0 讨论(0)
  • 2020-12-10 01:01

    Problem 1: Making OrderedSet a MutableCollection

    In a MutableCollection you can change an individual element (or a slice of elements) via a subscript that supports write access. And that is where the problems start: What should the output of

    var oset: OrderedSet = [1, 2, 3, 4]
    oset[0] = 3
    print(oset)
    

    be? We cannot simply replace the first element because then the set members are not unique anymore. Your current implementation returns [1, 2, 3, 4], i.e. it rejects the setting if the new member is already present in the set.

    That makes many default implementations of MutableCollection methods fail: sort(), swapAt(), shuffle() and probably more:

    var oset: OrderedSet = [4, 3, 2, 1]
    oset.swapAt(0, 2)
    print(oset) // [4, 3, 2, 1]
    oset.sort()
    print(oset) // [4, 3, 2, 1]
    oset.shuffle()
    print(oset) // [4, 3, 2, 1]
    

    Problem 2: Slicing

    In your implementation you chose Slice<OrderedSet<Element>> as the SubSequence type. Slice uses the storage from the originating (base) collection and only maintains its own startIndex and endIndex. That leads to unexpected results:

    let oset: OrderedSet = [1, 2, 3, 4, 5]
    var oslice = oset[0..<3]
    oslice[0] = 5
    print(oslice) // [1, 2, 3]
    

    Setting oslice[0] is rejected because the originating set contains the new member. That is certainly not expected. Sorting a slice

    var oset: OrderedSet = [6, 5, 4, 3, 2, 1]
    oset[0..<4].sort()
    print(oset) // [6, 5, 4, 3, 2, 1]
    

    fails because the sorted elements are written back one by one, and that fails because the members are already present in the set. The same happens with a slice assignment:

    var o1: OrderedSet = [1, 2]
    let o2: OrderedSet = [2, 1]
    o1[0..<2] = o2[0..<2]
    print(o1) // [1, 2]
    

    Another problem is that a slice oset[0..<3] does not conform to OrderedSetProtocol:It is a (mutable) collection, but for example not a SetAlgebra, so that it cannot be used to form unions, intersections, or symmetric differences.

    Do you really need the MutableCollection conformance?

    I would seriously consider not to adopt the MutableCollection protocol. That does not make the ordered set immutable: It only means that individual members cannot be modified via the subscript setter. You can still insert or remove elements, or form unions or intersections with other sets. Only for “complex” operations like sorting you have to go via an extra temporary set:

    var oset: OrderedSet = [4, 3, 2, 1]
    oset = OrderedSet(oset.sorted())
    print(oset) // [1, 2, 3, 4]
    

    The big advantage is that there is no unclear behavior anymore.

    Yes, I want the MutableCollection conformance!

    OK, you asked for it – let's see what we can do. We could try to fix this by “fixing” the subscript setter. One attempt is your commented out code:

        set {
            guard set.update(with: newValue) == nil else {
                insert(remove(at: elements.firstIndex(of: newValue)!), at: index)
                return 
            }
            elements[index] = newValue
        }
    

    This has the effect of moving an existing member to the given location, shifting other elements:

    var oset: OrderedSet = [1, 2, 3, 4]
    oset[0] = 3
    print(oset) // [3, 1, 2, 4]
    

    This seems to make most methods work correctly:

    var oset: OrderedSet = [4, 3, 2, 1]
    oset.swapAt(0, 2)
    print(oset) // [2, 3, 4, 1]
    oset.sort()
    print(oset) // [1, 2, 3, 4]
    oset.shuffle()
    print(oset) // [1, 4, 3, 2]
    

    and even the subscript sorting:

    var oset: OrderedSet = [6, 5, 4, 3, 2, 1]
    oset[0..<4].sort()
    print(oset) // [3, 4, 5, 6, 2, 1]
    

    But I see two disadvantages:

    • This side effect of the subscript setter might not be expected by the user.
    • It breaks the required O(1) conformance of the subscript setter.

    Another option is to leave the subscript setter as it is (i.e. reject invalid settings), and implement the problematic methods instead of using the default implementations of MutableCollection:

    extension OrderedSet {
        public mutating func swapAt(_ i: Index, _ j: Index) {
            elements.swapAt(i, j)
        }
    
        public mutating func partition(by belongsInSecondPartition: (Element) throws -> Bool) rethrows -> Index {
            try elements.partition(by: belongsInSecondPartition)
        }
    
        public mutating func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows {
            try elements.sort(by: areInIncreasingOrder)
        }
    }
    
    extension OrderedSet where Element : Comparable {
        public mutating func sort() {
            elements.sort()
        }
    }
    

    In addition we need to implement the subscript setter taking a range

    public subscript(bounds: Range<Index>) -> SubSequence
    

    so that a sorted slice is assigned back to the set as one operation, and not each element individually.

    That worked in my tests, but there is a risk that I overlooked something.

    And for the slicing I would make OrderedSet its own SubSequence type. That means that the elements are duplicated. This could be avoided by making the element storage an ArraySlice but – as we saw above – the set of distinct members has to be duplicated anyway, to avoid unwanted side effects when the originating set is mutated.

    The code

    This is what I have so far. It works correctly as far as I can tell, but needs more testing.

    Note that some methods need not be implemented, e.g. ExpressibleByArrayLiteral has already a default implementation in SetAlgebra, and various index calculations have default implementations because the Index is Strideable.

    public struct OrderedSet<Element: Hashable> {
        private var elements: [Element] = []
        private var set: Set<Element> = []
    
        public init() { }
    
    }
    
    extension OrderedSet {
        public init<S>(distinctElements elements: S) where S : Sequence, S.Element == Element {
            self.elements = Array(elements)
            self.set = Set(elements)
            precondition(self.elements.count == self.set.count, "Elements must be distinct")
        }
    }
    
    extension OrderedSet: SetAlgebra {
        public func contains(_ member: Element) -> Bool {
            set.contains(member)
        }
    
        @discardableResult public mutating func insert(_ newMember: Element) -> (inserted: Bool, memberAfterInsert: Element) {
            let insertion = set.insert(newMember)
            if insertion.inserted { elements.append(newMember) }
            return insertion
        }
    
        @discardableResult public mutating func remove(_ member: Element) -> Element? {
            if let oldMember = set.remove(member) {
                let index = elements.firstIndex(of: member)!
                elements.remove(at: index)
                return oldMember
            } else {
                return nil
            }
        }
    
        @discardableResult public mutating func update(with newMember: Element) -> Element? {
            if let member = set.update(with: newMember) {
                return member
            } else {
                elements.append(newMember)
                return nil
            }
        }
    
        public mutating func formUnion(_ other: Self) {
            other.elements.forEach { self.insert($0) }
        }
    
        public mutating func formIntersection(_ other: Self) {
            for element in elements {
                if !other.contains(element) {
                    remove(element)
                }
            }
        }
    
        public mutating func formSymmetricDifference(_ other: Self) {
            for member in other.elements {
                if set.contains(member) {
                    remove(member)
                } else {
                    insert(member)
                }
            }
        }
    
        public func union(_ other: Self) -> Self {
            var orderedSet = self
            orderedSet.formUnion(other)
            return orderedSet
        }
    
        public func intersection(_ other: Self) -> Self {
            var orderedSet = self
            orderedSet.formIntersection(other)
            return orderedSet
        }
    
        public func symmetricDifference(_ other: Self) -> Self {
            var orderedSet = self
            orderedSet.formSymmetricDifference(other)
            return orderedSet
        }
    
        public init<S>(_ elements: S) where S : Sequence, S.Element == Element {
            elements.forEach { insert($0) }
        }
    }
    
    extension OrderedSet: CustomStringConvertible {
        public var description: String { elements.description }
    }
    
    extension OrderedSet: MutableCollection, RandomAccessCollection {
    
        public typealias Index = Int
        public typealias SubSequence = OrderedSet
    
        public subscript(index: Index) -> Element {
            get {
                elements[index]
            }
            set {
                if !set.contains(newValue) || elements[index] == newValue {
                    set.remove(elements[index])
                    set.insert(newValue)
                    elements[index] = newValue
                }
            }
        }
    
        public subscript(bounds: Range<Index>) -> SubSequence {
            get {
                return OrderedSet(distinctElements: elements[bounds])
            }
            set {
                replaceSubrange(bounds, with: newValue.elements)
            }
    
        }
        public var startIndex: Index { elements.startIndex}
        public var endIndex:   Index { elements.endIndex }
    
        public var isEmpty: Bool { elements.isEmpty }
    } 
    
    extension OrderedSet {
        public mutating func swapAt(_ i: Index, _ j: Index) {
            elements.swapAt(i, j)
        }
    
        public mutating func partition(by belongsInSecondPartition: (Element) throws -> Bool) rethrows -> Index {
            try elements.partition(by: belongsInSecondPartition)
        }
    
        public mutating func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows {
            try elements.sort(by: areInIncreasingOrder)
        }
    }
    
    extension OrderedSet where Element : Comparable {
        public mutating func sort() {
            elements.sort()
        }
    }
    
    extension OrderedSet: RangeReplaceableCollection {
    
        public mutating func replaceSubrange<C>(_ subrange: Range<Index>, with newElements: C) where C : Collection, C.Element == Element {
    
            set.subtract(elements[subrange])
            let insertedElements = newElements.filter {
                set.insert($0).inserted
            }
            elements.replaceSubrange(subrange, with: insertedElements)
        }
    }
    

    Conclusion

    I already said, dropping the MutableCollection conformance would be the safer solution.

    The above works but is fragile: I had to “guess” which methods must be implemented because the default implementation does not work. If the MutableCollection protocol in the Swift standard library gets a new method with a default implementation. things can break again.

    0 讨论(0)
提交回复
热议问题