For a project of mine, instead of using OptionSet
I built a BitwiseSet
type that can be used in this way:
enum Side: Int {
case left, right, bottom, top
}
typealias Sides = BitwiseSet<Side>
// in usage:
let sides: Sides = [.left, .right]
I've been pretty happy with this.
In my opinion, this OptionSet
macro feels more heavyweight than necessary (both in conceptual complexity and declaration syntax), and has an uglier declaration syntax. The equivalent option-set to the above would look like this:
@OptionSet
struct Sides {
enum Options: Int {
case left, right, bottom, top
}
}
// in usage:
let sides: Sides = [.left, .right]
The only advantage of OptionSet
I can see is that the type of the set is the same type as its value, allowing you to omit the []
when there's a single value:
let sides: Sides = .left // no [] only works with OptionSet
Although honestly I'm not sure if this is an advantage or a fault. (I wonder now if this could be imitated for BitwiseSet
with some key path forwarding trickery.)
On the negative, with @OptionSet
the enum
has to be nested in the @OptionSet
type and has a rigid name. Most of the time I'd rather use an external enum
. I suppose an external enum could be "typealised" inside of the @OptionSet
type (will the macro allow that?), but that's just more boilerplate.
BitwiseSet implementation
/// BitwiseSet is a set of values stored as a bit mask. Elements in the set
/// must be RawRepresentable with an Int as a RawValue. Typically, the element
/// type is an enum based on Int:
///
/// enum State: Int {
/// case closed
/// case open
/// case mixed
/// }
/// var validStates: BitwiseSet<State> = [.open, .closed]
///
/// Since the storage for BitwiseSet is an Int, raw values of its element
/// must not exceed the number of bits in an Int. So on a 64-bit environment,
/// the valid range of raw values for its elements are `0 ..< 64`, and with
/// 32-bit it is `0 ..< 32`.
public struct BitwiseSet<Element>: SetAlgebra, Sequence, RawRepresentable, Hashable where Element: RawRepresentable, Element: Equatable, Element.RawValue == Int {
public var rawValue: Int
public init() {
rawValue = 0
}
public init(rawValue: Int) {
self.rawValue = rawValue
}
/// Initialize the set with one optional element, resulting either in a
///
/// single-member set or the empty set (if `nil`).
public init(_ member: Element?) {
self.rawValue = BitwiseSet.rawValue(for: member)
}
public var first: Element? {
return first(where: { _ in true })
}
public func intersects(_ other: BitwiseSet) -> Bool {
return rawValue & other.rawValue != 0
}
public func union(_ other: BitwiseSet) -> BitwiseSet {
return BitwiseSet(rawValue: rawValue | other.rawValue)
}
public func intersection(_ other: BitwiseSet) -> BitwiseSet {
return BitwiseSet(rawValue: rawValue & other.rawValue)
}
public func symmetricDifference(_ other: BitwiseSet) -> BitwiseSet {
return BitwiseSet(rawValue: rawValue ^ other.rawValue)
}
public mutating func formUnion(_ other: BitwiseSet) {
rawValue |= other.rawValue
}
public mutating func formIntersection(_ other: BitwiseSet) {
rawValue &= other.rawValue
}
public mutating func formSymmetricDifference(_ other: BitwiseSet) {
rawValue ^= other.rawValue
}
public func contains(_ member: Element) -> Bool {
return rawValue & BitwiseSet.rawValue(for: member) != 0
}
@discardableResult
public mutating func insert(_ newMember: Element) -> (inserted: Bool, memberAfterInsert: Element) {
let oldRawValue = rawValue
rawValue |= BitwiseSet.rawValue(for: newMember)
return (oldRawValue == rawValue, newMember)
}
@discardableResult
public mutating func remove(_ member: Element) -> Element? {
let oldRawValue = rawValue
rawValue &= ~BitwiseSet.rawValue(for: member)
return oldRawValue == rawValue ? nil : member
}
@discardableResult
public mutating func update(with newMember: Element) -> Element? {
let oldRawValue = rawValue
rawValue |= BitwiseSet.rawValue(for: newMember)
return oldRawValue == rawValue ? newMember : nil
}
/// The range of valid raw values for elements. This is determined by the
/// bit size of Int. On a 64-bit architecture, the range is `0 ..< 64`.
public static var supportedRangeOfMemberRawValues: CountableRange<Int> {
return 0 ..< MemoryLayout<RawValue>.size * 8
}
/// The raw value to use for storing the given element. This is
/// simply `1 << member.rawValue`. `member.rawValue` must be inside of
/// `supportedRangeOfMemberRawValues`.
public static func rawValue(for member: Element?) -> Int {
guard let member = member else { return 0 }
assert(supportedRangeOfMemberRawValues.contains(member.rawValue), "BitwiseSet is limited to elements having a raw value in the range \(supportedRangeOfMemberRawValues) (dependent on the current architecture). Value \(member.rawValue) for \(member) is out of range.")
return 1 << member.rawValue
}
/// The set that contains all possible elements values.
/// - Note: Implemented by attempting to create an element with
/// `init?(rawValue:)` from all supported raw values in
/// `supportedRangeOfMemberRawValues`, adding non-nil elements into
/// the set. This is not efficient if `init?(rawValue:)` is not.
/// Also, there is no cache.
public static var all: BitwiseSet {
var all = BitwiseSet()
for elementRawValue in BitwiseSet.supportedRangeOfMemberRawValues {
if let element = Element(rawValue: elementRawValue) {
all.insert(element)
}
}
return all
}
// MARK: Sequence
public struct Iterator: IteratorProtocol {
fileprivate var index = 0
fileprivate var remainingSet: BitwiseSet
fileprivate init(_ set: BitwiseSet) { remainingSet = set }
mutating public func next() -> Element? {
while !remainingSet.isEmpty {
defer { index += 1 }
if let element = Element(rawValue: index), remainingSet.contains(element) {
remainingSet.remove(element)
return element
}
}
return nil
}
}
public func makeIterator() -> Iterator {
return Iterator(self)
}
}
extension BitwiseSet: CustomStringConvertible {
public var description: String {
return "[\(map { "\($0)" }.joined(separator: ", "))]"
}
}