Add swap into Set Collection Type

Hey SE!

I did not see any proposal about swich an element of Set Collection, so here I am.
First, what I mean with "swich an element". Sometimes I use a Set as a Toggle Manager to persists what elements I turned "on". When this happen I always need to add the follow swich method to the Set.

extension Set {
    mutating func `switch`(_ element: Element) {
        if self.contains(element) {
            self.remove(element)
        } else {
            self.insert(element)
        }
    }
}

As it happens all the time, I am thinking if this is not a thing that already could be as part of Set struct.
What do you all think about it?

In set algebra, this is called the symmetric difference. Swift offers the methods symmetricDifference (nonmutating) and formSymmetricDifference (mutating). So, you would write:

set.formSymmetricDifference([element])
7 Likes

Also, if you really like your extension, you can spell it right if you want by using backticks:

extension SetAlgebra {
  mutating func `switch`(_ element: ArrayLiteralElement) {
    formSymmetricDifference([element])
  }
}
var bats: Set = ["🏏", "πŸ¦‡"]
bats.switch("🏏")  // {"πŸ¦‡"}
bats.switch("🏏")  // {"πŸ¦‡", "🏏"}
1 Like

I made something similar using a different approach: exposing the presence of an element as a boolean value through a subscript:

var ingredients: Set<String> = ["water", "potato", "carrot"]

// what this pitch is about:
ingredients["potato"].toggle() // insert or remove "potato"

// other usages:
ingredients["carrot"] = false // removes "carrot" from the set
ingredients["onion"] = true // inserts "onion"
ingredients["sugar"] = client.likesSweetenedThings
The SetAlgebra Extension
extension SetAlgebra {

	/// A subscript that returns a boolean indicating whether the member is
	/// present in the set. Assiging a boolean value to the subscript will
	/// insert or remove the member from the script.
	/// - Parameter member: An element to look for in the set.
	public subscript (member: Element) -> Bool {
		get {
			return contains(member)
		}
		set {
			if newValue {
				insert(member)
			} else {
				remove(member)
			}
		}
	}

}

Whether this belongs in the standard library, I have some doubts. I find it quite useful for what I'm doing, but those aren't exactly the semantics you'd expect of a set.

5 Likes

That getter is useless, right? To me, that just looks like another use case for set-only subscripts/properties.

The getter is needed when you want to call .toggle() on the boolean value (which this pitch is all about).

3 Likes

I see. I didn't realize you were actually trying to support

ingredients["potato"].toggle() 

So no, you're not talking about set-only properties. But you are talking about named subscripts, another one of my favorite missing features*! :smiley_cat:

And I'm in agreement that contains should be one of those, instead of a method.

var set: Set = [1, 2, 3]

XCTAssert(set.contains[3])

set.contains[1] = false
XCTAssertEqual(set, [2, 3])

var contains = set.contains

contains[3].toggle()
XCTAssertEqual(set, [2])

contains.set = { _, _, _ in }
contains[2].toggle()
XCTAssertEqual(set, [2])

var four: Set = [4]
withUnsafeMutablePointer(to: &four) {
  contains.pointer = $0
  XCTAssert(contains[4])
}

The way we can express this is not good enough, because it won't work on constants. Currently, you need to take an Objective-C-like approach, with functions/properties for the use case of get-only named subscripts

public extension SetAlgebra {
  var contains: Subscript<Self, Element, Bool> {
    mutating get {
      .init(
        &self,
        get: Self.contains,
        set: { set, element, newValue in
          if newValue {
            set.insert(element)
          } else {
            set.remove(element)
          }
        }
      )
    }
  }
}

The way we can implement it ourselves works, but it'll break at some point if you abuse it past the one-liner typical use case. That is not good enough. They need to be in the language.

/// An emulation of the missing Swift feature of named subscripts.
/// - Note: Argument labels are not supported.
public struct Subscript<Root, Index, Value> {
  public typealias Pointer = UnsafeMutablePointer<Root>
  public typealias Get = (Root) -> (Index) -> Value
  public typealias Set = (inout Root, Index, Value) -> Void

  public var pointer: Pointer
  public var get: Get
  public var set: Set
}

public extension Subscript {
  init(
    _ pointer: Pointer,
    get: @escaping Get,
    set: @escaping Set
  ) {
    self.pointer = pointer
    self.get = get
    self.set = set
  }

  subscript(index: Index) -> Value {
    get { get(pointer.pointee)(index) }
    nonmutating set { set(&pointer.pointee, index, newValue) }
  }
}

…* Now that we have callAsFunction, there's no reason for both () and [] syntax anymore. I say "named subscript", because that's what this kind of thing would be in older languages, but we probably should get rid of [] and allow assignment to the result of ()s

using pointers that aren't valid anymore :scream:

1 Like

:smile_cat:

This is what we resort to when inout arguments can't be used in subscripts, and subscripts can't be chained to initializers.

That’s... quite an extreme measure to dive into undefined behavior territory, especially when the next viable option is to use argument labelβ€”set[contains: value].toggle()

2 Likes

This is actually very useful for creating bindings to sets in SwiftUI:

struct SelectView: View {
    var items: [String] = ["A", "B", "C"]
    @State var selectedItems: Set<String> = []

    var body: some View {
        VStack {
            ForEach(items, id: \.self) { item in
                Toggle(isOn: self.$selectedItems[item]) {
                    Text(item)
                }
            }
        }
    }
}

In fact, I think these are exactly the semantics you expect from a set. Something is either in the set or not, true or false. It might still not belong in the standard library though – the implementation is pretty trivial, and it’s hard to get it wrong.

3 Likes

I would love to see this, as it's a natural extension to Bool.toggle(), and I think this sort of API makes the most sense:

extension SetAlgebra {
    
    /// insert the element if it's not in the set, otherwise remove it
    /// returns the contained state of the element after toggling
    @discardableResult
    public mutating func toggle(_ element: Element) -> Bool {
        if contains(element) {
            remove(element)
            return false
        } else {
            insert(element)
            return true
        }
    }

}

This would make it easy to do things like:

setOfStrings.toggle("string 1")
indices.toggle(42)
renderOptions.toggle(.disableHDR)

and so on.

7 Likes