Skip to content

Instantly share code, notes, and snippets.

@oskarirauta
Last active March 31, 2024 09:56
Show Gist options
  • Save oskarirauta/b2be3039293aeb27dde20797252c8b55 to your computer and use it in GitHub Desktop.
Save oskarirauta/b2be3039293aeb27dde20797252c8b55 to your computer and use it in GitHub Desktop.
OptionSetAssociated: Swift's OptionSet with associated value for members.
import Foundation
public protocol OptionSetAssociated: OptionSet where RawValue: FixedWidthInteger {
var store: [RawValue: Any] { get set }
}
extension OptionSetAssociated {
public init<T>(_ member: Self, value: T) {
self.init(rawValue: member.rawValue)
self.store[member.rawValue] = value
}
public init<T>(rawValue: RawValue, value: T) {
self.init(rawValue: rawValue)
self.store[rawValue] = value
}
fileprivate init(rawValue: RawValue, store: [RawValue: Any]) {
self.init(rawValue: rawValue)
self.store = store
}
fileprivate static func combinedStore(_ old: [RawValue: Any], new: [RawValue: Any]) -> [RawValue: Any] {
new.map {$0.key}.reduce(into: old) {
$0[$1] = new[$1] ?? old[$1]
}
}
fileprivate static func storeOverride(_ store: [RawValue: Any], member: Self?, value: Any?) -> [RawValue: Any] {
guard let member: RawValue = member?.rawValue else { return store }
var store: [RawValue: Any] = store
store[member] = value
return store
}
public func getValue<T>(for member: Self) -> T? {
self.store[member.rawValue] as? T
}
mutating public func formUnion(_ other: __owned Self) {
self = Self(rawValue: self.rawValue | other.rawValue, store: Self.combinedStore(self.store, new: other.store))
}
}
extension OptionSet where Self: OptionSetAssociated, Self == Element {
@discardableResult
public mutating func insert(
_ newMember: Element
) -> (inserted: Bool, memberAfterInsert: Element) {
let oldMember = self.intersection(newMember)
let shouldInsert = oldMember != newMember
var result = (
inserted: shouldInsert,
memberAfterInsert: shouldInsert ? newMember : oldMember)
if shouldInsert {
self.formUnion(newMember)
} else {
self.store = Self.storeOverride(
Self.combinedStore(self.store, new: newMember.store),
member: newMember, value: newMember.store[newMember.rawValue])
result.memberAfterInsert.store[newMember.rawValue] = newMember.store[newMember.rawValue]
}
return result
}
@discardableResult
public mutating func remove(_ member: Element) -> Element? {
var intersectionElements = intersection(member)
guard !intersectionElements.isEmpty else {
return nil
}
let store: [RawValue: Any] = self.store
self.subtract(member)
self.store = store
self.store[member.rawValue] = nil
intersectionElements.store = Self.storeOverride([:], member: member, value: store[member.rawValue])
return intersectionElements
}
@discardableResult
public mutating func update(with newMember: Element) -> Element? {
let previousValue: Any? = self.store[newMember.rawValue]
var r = self.intersection(newMember)
self.formUnion(newMember)
self.store[newMember.rawValue] = newMember.store[newMember.rawValue]
if r.isEmpty { return nil } else {
r.store = Self.storeOverride([:], member: newMember, value: previousValue)
r.store[newMember.rawValue] = previousValue
return r
}
}
}
extension OptionSetAssociated where Self: Sequence {
public typealias Iterator = OptionSetAssociatedIterator
public func makeIterator() -> OptionSetAssociatedIterator<Self> {
OptionSetAssociatedIterator(element: self)
}
public static func - (lhs: Self, rhs: Self) -> Self {
rhs.reduce(into: lhs) {
$0.remove($1)
}
}
public static func + (lhs: Self, rhs: Self) -> Self {
rhs.reduce(into: lhs) {
$0.insert($1)
}
}
public static func += (lhs: inout Self, rhs: Self) {
lhs = lhs + rhs
}
public static func -= (lhs: inout Self, rhs: Self) {
lhs = lhs - rhs
}
}
extension OptionSetAssociated { // subscripts
public subscript<T>(for member: Self) -> T? {
self.store[member.rawValue] as? T
}
}
import Foundation
public struct OptionSetAssociatedIterator<Element: OptionSetAssociated>: IteratorProtocol {
private let value: Element
public init(element: Element) {
self.value = element
}
private lazy var remainingBits: Element.RawValue = value.rawValue
private var bitMask: Element.RawValue = 1
public mutating func next() -> Element? {
while remainingBits != 0 {
defer { bitMask = bitMask &* 2 }
if remainingBits & bitMask != 0 {
remainingBits = remainingBits & ~bitMask
return Element(rawValue: bitMask, value: self.value.store[bitMask])
}
}
return nil
}
}
import Foundation
func removeOptional(from string: String) -> String {
guard string.hasPrefix("Optional("), var _string: String = string as String? else { return string }
while _string.hasPrefix("Optional(") { _string = _string.replacingOccurrences(of: "Optional(", with: "") }
while _string.hasSuffix(")") { _string.removeLast() }
return _string
}
func testOptionSet() {
var testset: TestSet = []
testset = [.int, .string("hello")]
testset.remove(.int)
testset.update(with: .string("world"))
testset.insert(.int(10))
testset += TestSet.int(15) + .bool // Requires conformance to Sequence
print(testset.description + "\n")
// This test shows how each member only carries it's own value in store
testset.forEach { // Also requires conformance to Sequence
print($0.rawValue.description + ": " + $0.store.reduce(into: [String](), {
$0.append(removeOptional(from: String(describing: $1.value)))
}).joined(separator: ", "))
}
print(testset.description)
}
import Foundation
extension TestSet: CustomStringConvertible, Sequence {
public var description: String {
var members: [String] = []
var vars: [String] = []
if self.contains(.bool) { members.append("bool" ) }
if self.contains(.int) { members.append("int") }
if self.contains(.string) { members.append("string") }
if self.contains(.optString) { members.append("Optional<String>") }
if let int: Int = self.int { vars.append("Int(" + int.description + ")") }
if let string: String = self.string { vars.append("String(" + string + ")") }
if let optString: String = self.optString { vars.append("Optional<String>(" + optString + ")")}
return "Members: " + (members.isEmpty ? ["none"] : members).joined(separator: ", ") + "\nVariables: " + vars.joined(separator: ", ")
}
}
import Foundation
public struct TestSet: OptionSetAssociated {
public typealias RawValue = Int
public let rawValue: RawValue
public var store: [RawValue : Any] = [:]
public init(rawValue: RawValue) {
self.rawValue = rawValue
}
}
extension TestSet { // Members
public static var bool: Self { Self(rawValue: 1 << 0) }
public static var int: Self { Self(rawValue: 1 << 1) }
public static var string: Self { Self(rawValue: 1 << 2) }
public static var optString: Self { Self(rawValue: 1 << 3) }
public static func int(_ value: Int) -> Self {
Self(.int, value: value)
}
public static func string(_ value: String) -> Self {
Self(.string, value: value)
}
public static func optString(_ value: String?) -> Self {
Self(.optString, value: value)
}
}
extension TestSet { // Member options
public var int: Int? {
self.getValue(for: .int) ?? ( self.contains(.int) ? Int() : nil )
}
public var string: String? {
self.getValue(for: .string) ?? ( self.contains(.string) ? String() : nil )
}
public var optString: String? {
self.getValue(for: .optString)
}
}
@oskarirauta
Copy link
Author

Creation is identical to OptionSet, except that struct also needs:
public var store: [RawValue : Any] = [:]

Example with TestSet shows how to associate values to members.

Results from testOptionSet():

Members: int, string
Variables: Int(10), String(world)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment