I can see why someone would like this, but I think it moves away from the core idea of this being intuitive and discoverable.
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.
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 namedtoggle(_:)
, 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. Methodsinsert() / 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.
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))
}
}
}
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.
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!