Как реализовать универсальный заказанный набор обобщенного типа c, ранее известный как NSMutableOrderedSet в нативном Swift? - PullRequest
2 голосов
/ 24 января 2020

Я пытаюсь реализовать универсальный тип c Mutable Ordered Set, и он должен соответствовать многим протоколам, чтобы вести себя так же, как Array и Set в Swift. Прежде всего для достижения sh того, что элемент типа generi c должен соответствовать Hashable, а структура generi c должна соответствовать RandomAccessCollection, SetAlgebra, ExpressibleByArrayLiteral, AdditiveArithmetic, RangeReplaceableCollection и MutableCollection. Я также хотел бы разрешить доступ к своим индексам SubSequence, добавив поддержку PartialRangeThrough, PartialRangeUpTo, PartialRangeFrom и UnboundedRange.

Это мое обобщенное c OrderedSet объявление структуры:

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

Несмотря на то, что это вопрос с самостоятельным ответом, я был бы очень признателен и поощряю новые ответы, некоторые отзывы по этому поводу реализация и / или предложения по ее исправлению / улучшению:

edit / update:

Метод sorted работает, как и ожидалось, но мутирующий sort даже не меняется Порядок элементов соответствует RandomAccessCollection и Element соответствует Comparable.

var numbers: OrderedSet = [15, 40, 10, 30, 60, 25, 5, 100]

numbers[0..<4]           // [15, 40, 10, 30]
numbers[0..<4].sorted()  // [10, 15, 30, 40]

numbers[0..<4].sort()    // [15, 40, 10, 30, 60, 25, 5, 100]

print(numbers) 
// Prints "[15, 40, 10, 30, 60, 25, 5, 100]"
// But it should print "[10, 15, 30, 40, 60, 25, 5, 100]"

Как это исправить?

Ответы [ 2 ]

4 голосов
/ 24 января 2020

Собственная реализация Swift изменяемого упорядоченного набора:

public protocol OrderedSetProtocol: MutableCollection,
                                    RandomAccessCollection,
                                    SetAlgebra,
                                    AdditiveArithmetic,
                                    RangeReplaceableCollection
                                    where Element: Hashable, Index == Int { }

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

Соответствует протоколу MutableCollection

Для добавления соответствия к MutableCollection протокол для вашей собственной пользовательской коллекции, обновите индекс вашего типа для поддержки доступа как для чтения, так и для записи. Значение, хранящееся в нижнем индексе экземпляра MutableCollection, должно впоследствии быть доступно в той же позиции. То есть для экземпляра изменяемой коллекции a, индекса i и значения x два набора назначений в следующем примере кода должны быть эквивалентны:

extension OrderedSet: MutableCollection {
    public subscript(index: Index) -> Element {
        get { elements[index] }
        set {
            guard set.update(with: newValue) == nil else {
                //
                // needs some implementation before returning
                // insert(remove(at: elements.firstIndex(of: newValue)!), at: index)
                //
                return 
            }
            elements[index] = newValue
        }
    }
}

Соответствует протоколу RandomAccessCollection

Протокол RandomAccessCollection добавляет дополнительные ограничения на связанные типы индексов и подпоследовательности, но в остальном не предъявляет никаких дополнительных требований к протоколу BidirectionalCollection. Однако для обеспечения гарантий сложности коллекции произвольного доступа либо индекс для вашего пользовательского типа должен соответствовать протоколу Strideable, либо вы должны реализовать методы index (_: offsetBy :) и distance (from: to :) с эффективностью O (1).

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` 
    public subscript<R: RangeExpression>(range: R) -> SubSequence where Index == R.Bound { .init(base: self, bounds: range.relative(to: self)) }

    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 }
}

Соответствует протоколу SetAlgebra

При реализации пользовательского типа, соответствующего протокол SetAlgebra, вы должны реализовать необходимые инициализаторы и методы. Чтобы унаследованные методы работали правильно, соответствующие типы должны соответствовать следующим аксиомам. Предположим, что S является пользовательским типом, который соответствует протоколу SetAlgebra, x и y являются экземплярами S, а e имеет тип S.Element - тип, который содержит набор.

S () == [ ]

x.intersection (x) == x

x.intersection ([]) == []

x.union (x) == x

x.union ([]) == x x.contains (e) подразумевает x.union (y) .contains (e)

x.union (y) .contains (e) подразумевает x.contains (e) || y.contains (e)

x.contains (e) && y.contains (e) тогда и только тогда, когда x.intersection (y) .contains (e)

x.isSubset ( из: y) подразумевает x.union (y) == y

x.isSuperset (of: y) подразумевает x.union (y) == x

x.isSubset (of: y) тогда и только тогда, когда y.isSuperset (of: x)

x.isStrictSuperset (of: y) тогда и только тогда, когда x.isSuperset (of: y) && x! = y

x.isStrictSubset (of: y) тогда и только тогда, когда 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 { filter(other.contains) }
    public func symmetricDifference(_ other: Self) -> Self { filter { !other.set.contains($0) } + other.filter { !set.contains($0) } }

    public mutating func formUnion(_ other: Self) { other.forEach { self.append($0) } }
    public mutating func formIntersection(_ other: Self) { self = intersection(other) }
    public mutating func formSymmetricDifference(_ other: Self) { self = symmetricDifference(other) }
}

Соответствует ExpressibleByArrayLiteral

Добавьте возможность инициализации с помощью литерала массива для ваших собственных пользовательских типов, объявив инициализатор init (arrayLiteral :). В следующем примере показан инициализатор литерала массива для гипотетического типа OrderedSet, который имеет подобную множеству семантику, но поддерживает порядок своих элементов.

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

Соответствует AdditiveArithmeti c Protocol

Чтобы добавить соответствие AdditiveArithmeti c протокола вашему собственному пользовательскому типу, реализовать необходимые операторы и предоставить нулевое свойство stati c, используя тип, который может представлять величину любого значения вашего пользовательского типа.

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) }
}

В соответствии с протоколом RangeReplaceableCollection

Чтобы добавить соответствие RangeReplaceableCollection вашему пользовательскому коллекции, добавьте пустой инициализатор и метод replaceSubrange (: with :) к своему пользовательскому типу. RangeReplaceableCollection предоставляет реализации по умолчанию всех других своих методов, использующих этот инициализатор и метод. Например, метод removeSubrange ( :) реализуется путем вызова replaceSubrange (_: with :) с пустой коллекцией для параметра newElements. Вы можете переопределить любой из требуемых методов протокола, чтобы обеспечить свою собственную реализацию.

extension OrderedSet: RangeReplaceableCollection {

    public init<S: Sequence>(_ elements: S) where 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)
        var index = subrange.relative(to: self).lowerBound
        newElements.forEach {
            if set.insert($0).inserted {
                elements.insert($0, at: index)
                formIndex(after: &index)
            }
        }
    }


Соответствует протоколу CustomStringConvertible

Добавьте соответствие CustomStringConvertible к вашим пользовательским типам, определив свойство description.

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

Соответствует его Slice до CustomStringConvertible, а также:

extension Slice: CustomStringConvertible where Base: OrderedSetProtocol {
    public var description: String {
        var description = "["
        var first = true
        for element in self {
            if first {
                first = false
            } else {
                description += ", "
            }
            debugPrint(element, terminator: "", to: &description)
        }
        return description + "]"
    }
}

Quick Playground Test (OrderedSet должен иметь все методы, доступные для Swift собственных Array и Set структур)

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]
ordereSet1.insert(contentsOf: [1,3,4,6], at: 0)   // [4, 6, 1, 2, 3]

ordereSet2.popLast()                              // 9                        
1 голос
/ 31 января 2020

Проблема 1: Создание OrderedSet a MutableCollection

В MutableCollection вы можете изменить отдельный элемент (или часть элементов) через индекс, который поддерживает write доступ. И вот тут начинаются проблемы: каким должен быть вывод

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

? Мы не можем просто заменить первый элемент, потому что тогда члены набора больше не являются уникальными. Ваша текущая реализация возвращает [1, 2, 3, 4], т.е. она отклоняет настройку, если новый член уже присутствует в наборе.

Это приводит к сбою многих реализаций по умолчанию MutableCollection методов: sort(), swapAt(), shuffle() и, возможно, больше:

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]

Проблема 2: Нарезка

В своей реализации вы выбрали Slice<OrderedSet<Element>> в качестве типа SubSequence. Slice использует хранилище из исходной (базовой) коллекции и поддерживает только свои собственные startIndex и endIndex. Это приводит к неожиданным результатам:

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

Настройка oslice[0] отклонена, поскольку исходный набор содержит новый элемент. Это, конечно, не ожидается. Сортировка фрагмента

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

завершается неудачно, потому что отсортированные элементы записываются обратно по одному, и не получается, потому что члены уже присутствуют в наборе. То же самое происходит с назначением среза:

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

Другая проблема заключается в том, что срез oset[0..<3] не соответствует OrderedSetProtocol: это (изменяемая) коллекция, но, например, не SetAlgebra , так что его нельзя использовать для образования союзов, пересечений или симметрий c различий.

Вам действительно нужно соответствие MutableCollection?

Я бы серьезно подумал , а не для принятия протокола MutableCollection. Это не делает упорядоченный набор неизменным: это означает только то, что отдельные элементы не могут быть изменены с помощью установщика нижнего индекса. Вы по-прежнему можете вставлять или удалять элементы, или объединять или пересекать с другими наборами. Только для «сложных» операций, таких как сортировка, вам нужно go с помощью дополнительного временного набора:

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

Большим преимуществом является то, что больше нет неясного поведения.

Да, я хочу соответствия MutableCollection!

ОК, вы просили об этом - давайте посмотрим, что мы можем сделать. Мы могли бы попытаться исправить это, «исправив» установщик индекса. Одной из попыток является ваш закомментированный код:

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

В результате перемещает существующего члена в указанное место, перемещая другие элементы:

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

Это кажется , чтобы заставить большинство методов работать правильно:

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]

и даже сортировка подстрочного индекса:

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

Но я вижу два недостатка:

  • Этот побочный эффект от установщика индекса может не ожидаться пользователем.
  • Он нарушает требуемое O (1) соответствие установщика индекса.

Другой вариант это оставить установщик индекса как есть (то есть отклонить неверные настройки) и реализовать проблемные c методы вместо использования реализаций по умолчанию 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()
    }
}

Кроме того, нам нужно реализовать индекс setter с диапазоном

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

, чтобы отсортированный срез был назначен обратно в набор как одна операция, а не каждый элемент в отдельности.

Это работало в моем тесты, но есть риск, что я что-то упустил.

А для нарезки я бы сделал OrderedSet свой собственный тип SubSequence. Это означает, что элементы дублируются. Этого можно избежать, сделав element хранилище ArraySlice, но, как мы видели выше, set отдельных элементов в любом случае необходимо дублировать, чтобы избежать нежелательных побочных эффектов при мутации исходного набора.

Код

Это то, что у меня есть до сих пор. Насколько я могу судить, он работает правильно, но требует дополнительного тестирования.

Обратите внимание, что некоторые методы не нужно реализовывать, например, ExpressibleByArrayLiteral уже имеет реализацию по умолчанию в SetAlgebra, а различные вычисления индекса имеют значения по умолчанию. реализации, потому что Index является 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)
    }
}

Заключение

Я уже сказал, что отказ от соответствия MutableCollection будет более безопасным решением.

Выше работает, но это fr agile: мне пришлось «угадать», какие методы должны быть реализованы, потому что реализация по умолчанию не работает. Если протокол MutableCollection в стандартной библиотеке Swift получает новый метод с реализацией по умолчанию. вещи могут снова сломаться.

...