[Pitch] Add `insertOrRemove(_:)` Method to `Set` and `SetAlgebra` in Swift

I can see why someone would like this, but I think it moves away from the core idea of this being intuitive and discoverable.

1 Like

The subscript that returns a Bool is cool, because it can be used in key paths.

In SwiftUI for example, one can toggle the presence of an element with a plain Toggle:

@State var mySet: Set<MyOption>

Toggle("Option 1", $mySet[member: .option1])
Toggle("Option 2", $mySet[member: .option2])

(No key path is visible: it is hidden in the dynamic member lookup that provides to the $mySet binding the same properties and subscript as its wrapped set.)

This technique avoids the creation of bindings with a get/set pair, which has caveats.

10 Likes

Big +1.

While I greatly like the feature in common, I feel that toggle(_:) is a poor name for several reasons:

  • toggle is mentally associated with Bool values.
    In reality, the choice here is between some value of type T and nil, not true / false
  • 'toggle' is totally misleading for beginners.
    In that time, insertOrRemove(_:) is also not 100% descriptive about what it does, but much more intuitive and clear.
  • SetAlgebra<Bool> Types will become weird. Imagine Set or BitSet. Both collection itself and its elements have method named toggle(_:), but these methods do very different things – first remove value from the collection or insert it, the second one change value between true / false.
  • toggle(_ element:) name assumes that collection apply some operation to provided element, but this is not true. Methods insert() / remove() change the collection itself, not its element. In contrary, toggle(_ element:) is not felt like it also change the collection itself, at a first glance it is understood as it changes the given element.

For these reasons, insertOrRemove(_:) seems to be more clear. It is also the most similar to the operations it does – insert() / remove(), and very discoverable with autocomplete.

One interesting topic to discuss whether this method should return something, and if yes what should it be.

The underlying methods have these signatures:
insert(_ newMember: Element) -> (inserted: Bool, Element) – when using inside insertOrRemove(:), only (true, newMember) execution path is possible.
remove(_ member: Self.Element) -> Self.Element? – when using inside insertOrRemove(
:), nil value is impossible.

So ideally instead of using Bool values with a combination of Optional value, we should have a enum with associated value:

enum InsertOrRemoveResult {
  case inserted
  case removed(_ oldElement: Element)
}

While such a enum is very descriptive, it increases the number of public types in standard library.
Usage:

if set.insertOrRemove("A")  == .inserted {
  // inserted
} else {
  // removed
}

if case .removed(let oldValue) = set.insertOrRemove("A") {
  // removed
} else {
  // inserted
}

switch set.insertOrRemove("A") {
  case .inserted: // inserted
  case .removed(let oldValue): // removed
}

Technically, we can use Optional for this purpose:

if set.insertOrRemove("A")  == nil {
  // inserted
} else {
  // removed
}

if let removedOldValue = set.insertOrRemove("A") {
  // removed
} else {
  // inserted
}

switch set.insertOrRemove("A") {
  case .none: // inserted
  case .some(let removedOldValue): // removed
}

I personally think that InsertOrRemoveResult is little better than Optional, but this ergonomic boost is not so much. This is a narrow feature, it is frequently requested but rarely used – I mean its usage in codebases is much less than operations like insert / remove / update, and Invention of special result type seems to be an overkill.

Finally, I think using Optional as a result type is a right balance.

4 Likes

I often use this for SwiftUI:

public extension Binding {
  /// Given a binding to a Set, returns a Binding<Bool> that checks for/ensures containment of an element in that set.
  func contains<Element>(_ element: Element) -> Binding<Bool> where Value == Set<Element> {
    .init(
      get: { wrappedValue.contains(element) },
      set: { isIncluded in
        if isIncluded { 
          wrappedValue.insert(element) 
        } else { 
          wrappedValue.remove(element) 
        }
      }
    )
  }
}

Is allows me to use a @Bidning vat set: Set<Stuff> and wend a Bool-binding:

struct SomeView: View {
  @State var items: Set<Stuff> = []
  
  public var body: some View {
    ForEach(Stuff.allCases) { thing in 
      Toggle(thing.description, isOn: $item.contains(thing))
    } 
  }
}
1 Like

Sorry for being a downer, but I would take a purist approach here.

It may have swayed me if we took an efficiency angle to this. That would require having a small little benchmarking runs over a forked stdlib to see if it would result in significant enough speedup.

This optimization wouldn’t apply to existential any SetAlgebra, as it would probably be backwards incompatible. But who uses any SetAlgebra anyway??

If we were talking about efficiency, an entry-like API would’ve been personally preferable (though isn’t backwards compatible, would probably require ~Escapable)

As it stands, it mostly looks like bikeshedding on three lines long extension. Personally it is not a big deal for me. Maybe the presence of this extension in codebases is widespread enough that adding it to the language does make sense ĀÆ_(惄)_/ĀÆ.

Also. Do we have any comparisons with approaches other language ecosystems took? Not like we should make same decisions as others, but to compare and contrast. From what I’ve seen, most don’t provide toggle functionality. Although Haskell’s Data.Set.alterF is a generalisation of that.

2 Likes

I don’t see a method named toggled in the documentation for Bool. There is Bool.toggle(), but that’s not equivalent to ! because it mutates the boolean instead of returning the toggled boolean.

To my knowledge, the only place in the standard library where a method and operator do exactly the same thing is append and + for RangeReplaceableCollection types.

Sorry, how do you use a function like the pitched insertOrRemove or the xor from my post as a keypath?

Hi everyone,

Thank you all for the thoughtful feedback and engaging discussion on my pitch! I’ve been working through the suggestions and considering updates based on the input so far.

As this is my first pitch, I’d love some guidance on what the next steps are to move this forward. Should I start drafting a formal proposal now, or is there anything else I should address first to align with the process?

I appreciate any insights or advice you can share—thanks again for your support and feedback so far!

1 Like