Set, OrderedSet, SetAlgebra, and SwiftUI

I ran into an issue recently where I wanted a SwiftUI list multiple selection to preserve the order in which the user made the selection. You can bind the List to a Set object to enable multi-selection behavior. I thought I was so clever to go grab an OrderedSet, but it turns out List really wants a Set, and not something that’s just set-like.

Looking into it further, while Set conforms to SetAlgebra, OrderedSet does not.

Would it make sense (is it even possible) for OrderedSet to conform to SetAlgebra? A comment in the code says:

// OrderedSet does not directly conform to SetAlgebra because its definition
// of equality conflicts with SetAlgebra requirements. However, it still
// implements most SetAlgebra requirements (except insert, which is replaced
// by append).

Would it also make sense for SwiftUI to accept containers that conform to SetAlgebra, rather than Set specifically?

I realize I can’t really pitch SwiftUI enhancements here, I just mention it for motivation. Maybe this isn't even Evolution-worthy. But I feel like there’s some harmonization to be had here.

I'm not even sure SwiftUI can be enhanced to just accept OrderedSet as well as Set, since that would create a dependency on a third-party library. (I feel like I've seen a way to make something available in a library if the client has included the dependency, without forcing that dependency on the client, but that seems magical.)

3 Likes

As stated, it doesn't make sense that OrderedSet conforms to SetAlgebra because of the equatable expectations:

let a: OrderedSet = [1,2,3]
let b: OrderedSet = [3,2,1]

print(a == b) // false

let c: Set = [1,2,3]
let d: Set = [3,2,1]

print(c == d) // true

Now, whether or not there should be a SetAlgebra-conforming view (like elements) is a different question.

3 Likes

I don’t actually see where equality is defined. I get that the question of comparing a Set to an OrderedSet could be complicated, or even nonsensical. Perhaps they are never strictly equal. Perhaps equality could be defined to mean "disregarding order" with additional methods for testing equality including order.

It already seems to have some accommodations for OptionSet, wouldn’t it make sense to accommodate ordering as well?

The Equality conformance is just a basic equality of the underlying ContiguousArray:

Where are these equatable expectations stated / documented?


Two ideas IRT how to do OrderedSet based selection:

  1. start with having a way to construct a new ordered set from existing:
extension OrderedSet {
    mutating func change(to newValue: Set<Element>) {
        for old in self {
            if !newValue.contains(old) {
                remove(old)
            }
        }
        for new in newValue {
            if !contains(new) {
                append(new)
            }
        }
    }
}

this will not effect the order of existing items, items no longer in the set will be removed and new items will be added to the end.

Then, as SwiftUI wants "Set" give it a set made out of orderedSet:

    private var cachedSet: Set<ID>!
    
    var orderedSet: OrderedSet<ID> = [] {
        didSet { cachedSet = nil }
    }
    
    var selection: Set<ID> {
        get {
            if cachedSet == nil {
                cachedSet = Set(orderedSet)
            }
            return cachedSet
        }
        set {
            orderedSet.change(to: newValue)
            cachedSet = newValue
        }
    }

Make sure that "cachedSet" value is getting cleared if you change orderedSet.
This should satisfy SwiftUI and hopefully the price of converting from OrderedSet to Set after cached value invalidation will not be too high.

  1. Ignore SwiftUI's "selection" and implement selection logic yourself (in which case you could use anything including OrderedSet. It's not hard at all.
1 Like

I probably made my comment while you were typing, but see above the OrderedSet: Equatable conformance file.


OrderedSet is defined so that elements are in a, well, order. It doesn't really make theoretical sense such that equality ignores that compared to the logical/mathematical expectations of Set<Algebra>.

1 Like

I meant: where are these logical/mathematical expectations of SetAlgebra are stated / documented? I wasn't able finding them in the SetAlgebra protocol.

Not hard? I'd sure love some hints on how to do this, as I'm having trouble imagining how I'd go about it.

I've definitely considered trying to keep track of the selection order as a side-effect of changing selection, but I'm not sure that works reliably (e.g. what happens if the user shift-clicks several items away from the current selection? I think you only get a single change notification, and no guarantee of the order of the items. You'd have to do some careful inspection of the list order to get it right.

1 Like

logical/mathematical expectations

It's quite implicit that these are expected to be mathematical.

Right. If in the listed set of axioms would be, say:

[a, b] == [b, a]

That would prohibit OrderedSet outright, I just don't see anything like that in that list of axioms.

1 Like

From the OrderedSet documentation:

[...] OrderedSet has an order-sensitive definition of equality (see above) that is incompatible with SetAlgebra 's documented semantic requirements. Accordingly, OrderedSet does not (cannot) itself conform to SetAlgebra .

2 Likes

This "SetAlgebra 's documented semantic requirements" is the droid I am looking for :grinning: Could you point me to it? Specifically those that specify that SetAlgebra conformer should maintain [a, b] = [b, a] or something to that account.

1 Like

You are right. Being iOS inclined I completely forgot about the physical keyboard with shift, etc. :man_facepalming:

What do you think about method (1) from the post above?

It follows from the axioms.

For example look at

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

which is true for Set but not for OrderedSet:


do {
    let x: Set = [0, 1]
    let y: Set = [1, 0]
    precondition(x.isSubset(of: y))
    // succeeds (not actually a proof, just an example for illustration purposes)
    precondition(x.union(y) == y)
}

do {
    let x: OrderedSet = [0, 1]
    let y: OrderedSet = [1, 0]
    precondition(x.isSubset(of: y))
    // fails because of the order difference between x and y (counter example) 
    precondition(x.union(y) == y)
}
5 Likes

Couldn’t x.union(y) be implemented such that it returns the y ordering? It seems the order returned here is arbitrary, as union can’t honor both orderings. I don't know if that would lead to consistency for all cases, but it's not clear to me how union should behave for ordered sets.

Alternatively, can there be a protocol for "ordered-without-duplicates" that can implement a subset of SetAlgebra?

Nice, thanks for deciphering this.

Is there another "weaker" protocol, say, "SetProtocol" that both Set and OrderedSet would conform to? If so, SwiftUI could have used that instead of hardcoding "Set".


OTOH, would it be a great UX when user sees a number of selected items and has no clue what the order is among them (whilst this order matters somehow)? Maybe there needs to be a clue to the order via some small (1), (2), etc labels shown on the items?


    mutating func change(to newValue: Set<Element>) {
        ...
        for new in newValue {
            if !contains(new) {
                append(new)
            }
        }
    }

In light of adding multiple items (e.g. via a shift click) this fragment needs to be changed to go in the current-on-screen sorting order of items. As written the order of the newly added items will be random.

1 Like

I think the crux of the matter is that the equality operator, ==, is expected to be commutative. i.e. symmetrical w.r.t. to its operands. So it has no intrinsic bias towards the semantics of one operand or the other.

An approach involving methods (not operators) might be viable, though. e.g. if unordered sets had a hasSymmetricDifference(:) method.

In any case a new Setish protocol would have to be added, to represent a more limited set of constraints than what SetAlgebra requires.

1 Like

I'm not sure about "documented" in Swift, but the mathematical semantics is certainly well-known and thus implicitly expected: Algebra of sets - Wikipedia

Here, you'll find that you'll have to ignore any order, if you want subsets and unions to work axiomatically.

For ordered sets, it is certainly possible to implement a set algebra, but that would then either have to break expectations of equality, or established expectations of how subsets work for ordered sets.

2 Likes

You can simply add a lens on your ordered set, that vends an ordinary set and only mutates the underlying ordered set when needed:

extension OrderedSet {
    var elements: Set<Element> {
        get { Set(self) }
        set { self.update(withElementsFrom: newValue) }
    }
    private mutating func update(withElementsFrom other: Set<Element>) {
        for old in self {
            if !other.contains(old) { remove(old) }
        }
        for new in other {
            if !self.contains(new) { append(new) }
        }
    }
}

Then you can simply use it as so:

import SwiftUI
import OrderedCollections

struct ListView: View {
    @State private var selection: OrderedSet<Int> = []
    //                            ^^^^^^^^^^

    var body: some View {
        VStack {
            List(
              Array(0..<1000),
              id: \.self,
              selection: $selection.elements,
              //                   ^^^^^^^^^
              rowContent: { i in Text("Row \(i)") }
            )
            Text("Selection: \(selection.description)")
        }
    }
}
2 Likes

That's a good option too.

Although, I'm not sure it's wise to have elements be settable. The order in which new elements will be added is undefined. It somewhat defeats the point of an ordered collection to add items in random order. Perhaps better to not have that 'convenience' and instead require users to add the elements manually, thus encouraging them to think about the order of addition.

1 Like