This is probably best explained with a use case.
I have a logger class. It logs messages to outputs at levels.
class Logger {
var outputs: OutputOptions
var filter: Level
func log(_ message: String, at level: Level) {
if level <= self.filter {
outputs.log(message)
}
}
}
The possible outputs are defined by an OptionSet and determines to which outputs (NSLog, Instabug, etc) messages should be logged. OptionSet is nice here because I can define select multiple outputs and easily check which are selected when logging.
struct OutputOptions: OptionSet {
let rawValue: Int
static let console = OutputOptions(1 << 0)
static let instabug = OutputOptions(1 << 1)
func log(_ message: String) {
if self.contains(.console) {
NSLog(message)
}
// etc
}
}
Levels are defined by an enum and denote levels of message, such as error, warning, info, etc. Loggers can filter out messages above a certain level if we're not interested in getting a verbose output. The logger's filter is also set to a level.
enum Level: Int {none, no logs are shown. */
case none = 0
case error = 1
case warning = 2
case info = 3
case verbose = 4
}
I would like to combine output options and levels in some way, allowing me to specify that certain outputs can filter messages to a certain level, while other outputs can filter to other levels. For example, I would like to log verbose messages to console, but only errors to Instabug. On the surface, OptionSets looks like combine-able enums, so my mind immediately went to associated values. If each option could have an associated filter level, I could set a logger's output like this:
let logger = Loggger()
logger.outputs = [.console(filter: .verbose), .instabug(filter: .error)]
In trying to get this to work, I added a filter property to the OutputOptions. My options now look like this:
struct OutputOptions: OptionSet {
let rawValue: Int
var filter: Level = .info
init(rawValue: Int, filter: Level) {
self.rawValue = rawValue
self.filter = filter
}
static func console(filter: Level = .info) {
return OutputOptions(rawValue: 1 << 0, filter: filter)
}
// etc
BUT I can't figure out how to access the filter variable of an element in log
. Based on my experience with enums, I would have expected to be able to do
func log(_ message: String, at level: Level) {
if self.contains(.console(let filter)) { // <== does not compile!
if level <= filter {
NSLog(message)
}
}
}
}
But that does not compile. In fact, it looks like the filter
property is not separately defined for each option, but rather for a whole option set.
SO: Is there a way to associate values with individual options in an option set?
But that does not compile. In fact, it looks like the filter property is not separately defined for each option, but rather for a whole option set.
This is because an OptionSet isn't really a set, per se. If I have the following OptionSet:
struct MyOptions: OptionSet {
let rawValue: Int
static let foo = MyOptions(1 << 0)
static let bar = MyOptions(1 << 1)
}
and then I make the set like so:
let opts: MyOptions = [.foo, .bar]
I don't actually have a collection with two MyOptions
instances in it. Instead, I have a new instance of MyOptions
whose rawValue
is set to (.foo.rawValue | .bar.rawValue)
—i.e. 3. The original two MyOptions
instances are discarded as soon as opts
is made.
Similarly, your logger.outputs
will be an instance of OutputOptions
with rawValue
3 and the default value for filter
.
Thus, it's not really possible to do what you want with an OptionSet
.
This isn't actually true, OptionSet can have associated value(s) just fine but it will require some work.. Luckily not so much and here's a simple sample.
This is OptionSetIterator, which is not actually needed for this, but will aid in demonstration, it's from here.
public struct OptionSetIterator<Element: OptionSet>: IteratorProtocol where Element.RawValue == Int {
private let value: Element
private lazy var remainingBits = value.rawValue
private var bitMask = 1
public init(element: Element) {
self.value = element
}
public mutating func next() -> Element? {
while remainingBits != 0 {
defer { bitMask = bitMask &* 2 }
if remainingBits & bitMask != 0 {
remainingBits = remainingBits & ~bitMask
return Element(rawValue: bitMask)
}
}
return nil
}
}
extension OptionSet where Self.RawValue == Int {
public func makeIterator() -> OptionSetIterator<Self> { OptionSetIterator(element: self) }
}
And here is a actual code sample for OptionSet with associated values:
public struct MyOptions: OptionSet, Equatable, Sequence, CustomStringConvertible {
public let rawValue: Int
public fileprivate(set) var tag: Int?
public fileprivate(set) var text: String?
mutating public func formUnion(_ other: __owned MyOptions) {
self = Self(rawValue: self.rawValue | other.rawValue, tag: other.tag ?? self.tag, text: other.text ?? self.text)
}
@discardableResult
public mutating func insert(
_ newMember: Element
) -> (inserted: Bool, memberAfterInsert: Element) {
let oldMember = self.intersection(newMember)
let shouldInsert = oldMember != newMember
let result = (
inserted: shouldInsert,
memberAfterInsert: shouldInsert ? newMember : oldMember)
if shouldInsert {
self.formUnion(newMember)
} else {
self.tag = newMember.tag ?? self.tag
self.text = newMember.text ?? self.text
}
return result
}
@discardableResult
public mutating func remove(_ member: Element) -> Element? {
let intersectionElements = intersection(member)
guard !intersectionElements.isEmpty else {
return nil
}
let tag: Int? = self.tag
let text: String? = self.text
self.subtract(member)
self.tag = tag
self.text = text
return intersectionElements
}
private init(rawValue: Int, tag: Int?, text: String?) {
self.rawValue = rawValue
self.tag = tag
self.text = text
}
public init(rawValue: Int) {
self.rawValue = rawValue
self.tag = nil
self.text = nil
}
private static var _tag: Int { 1 << 0 }
private static var _text: Int { 1 << 1 }
public static func tag(_ value: Int) -> MyOptions {
MyOptions(rawValue: _tag, tag: value, text: nil)
}
public static func text(_ value: String) -> MyOptions {
MyOptions(rawValue: _text, tag: nil, text: value)
}
public var description: String {
var modes: [String] = []
self.forEach {
var text: String = ""
switch $0.rawValue {
case MyOptions._tag: text = "tag{" + ( self.tag?.description ?? "nil" ) + "}"
case MyOptions._text: text = "text=" + ( self.text ?? "nil" )
default: text = "unknown"
}
modes.append(text)
}
guard !modes.isEmpty else { return "none" }
guard modes.count > 1 else { return modes.first ?? "error" }
return "[" + modes.joined(separator: ", " ) + "]"
}
}
In this sample, MyOptions does not need conform to Equatable and Sequence, they are for OptionSetIterator. Also CustomStringConvertible is there only for demonstration purposes, as is also variable description.
And here's some tests:
var options: MyOptions = [
.text("hello"),
.text("world"),
.tag(10)
]
options.insert(.tag(9))
options.update(with: .tag(0))
options.remove(.tag(-1))
print("Options: " + options.description)
Results:
Options: text=world
When removing member from set, it's associated values are kept. With small changes, they can be also cleared when member leaves the set, but if you are not storing big amounts of data in your set's associated types... I usually use OptionSet as a configurator for my classes and structs with optional options, so I rarely remove members from set - so I opted that out just out of my laziness..
This is overriding default methods with minor changes. Original source for OptionSet is available at github.com
Any comments or improvements..?
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With