Last night Zev Eisenberg was asking about option sets. “Do you still have to specify 1 << _n_
manually for OptionSet
conformance? There’s no magic?” So I decided to build him some magic. There’s really no reason you should have to manually put in numbers like this:
public struct Traits: OptionSet { public typealias RawValue = Int public var rawValue = 0 public init(rawValue: Traits.RawValue) { self.rawValue = rawValue } public static let bolded = 1 << 0 public static let italicized = 1 << 1 public static let monospaced = 1 << 2 public static let underlined = 1 << 3 public static let outlined = 1 << 4 }
This approach requires unnecessary bookkeeping. You have to keep track of the bits you’ve used, especially if you add or insert new options, or reorder the options you have. It gives unnecessary prominence to the implementation detail values. There should be a more magic way.
So I decided to write him a solution that automatically generated the bit flags and hid those details from the implementation. The result looks like this:
public static let bolded = generateOption() public static let italicized = generateOption() public static let monospaced = generateOption() public static let underlined = generateOption() public static let outlined = generateOption()
You can move things around, add new items, delete old items. It really doesn’t make a difference from a code maintenance point of view (assuming you’re doing this all during development, not after deployment, where you’d want to use availability and deprecations).
To get here, I needed to create a function that would add type-specific options to any type that conforms to OptionSet
. I created a global dictionary to store option counts:
private var _optionSetDict: [AnyHashable: Int] = [:]
To be honest, I hate unparented globals like this. However, Swift does not allow adding static stored values in extensions. I couldn’t think of another better way to handle this. I also built a second global to ensure this dictionary would prevent concurrent access, so my counts would be secure:
private var _optionSetAccessQueue = DispatchQueue( label: "sadun.OptionSetGeneration", attributes: [.concurrent])
I needed to box my type references since Swift doesn’t allow types to conform to Hashable
. They won’t work out of the box with dictionaries. This solution let me use types as keys:
/// Wraps a type to enable it for use as a dictionary key
public struct TypeWrapper<Wrapped>: Hashable {
private let type: Wrapped.Type
public init(_ type: Wrapped.Type) {
self.type = type
}
/// Conforms to Equatable
public static func ==(lhs: TypeWrapper, rhs: TypeWrapper) -> Bool {
return lhs.type == rhs.type
}
/// Conforms to Hashable
public var hashValue: Int {
return ObjectIdentifier(type).hashValue
}
}
To create a hashable type entry, I just instantiate TypeWrapper
with the type.
Sven Weidauer points out I can use ObjectIdentifier
directly
Here’s the OptionSet
extension that implements the generateOption()
magic:
public extension OptionSet where RawValue == Int { public static func generateOption() -> Self { let key = ObjectIdentifier(Self.self) return _optionSetAccessQueue.sync(flags: [.barrier]) { // This should be locked so there's a guarantee that // counts are unique for each generated option let count = _optionSetDict[key, default: 0] _optionSetDict[key] = count + 1 return .init(rawValue: 1 << count) } } }
I’m not sure that I’d ever actually use this approach in code but it was a fun exercise in problem solving. Sven W. adds “Another thing to keep in mind is that statics are initialised the first time they are used. So in different runs of the program the values can differ. Better not persist OptionSets
created by this technique.”
You can see the full implementation over at Github. And if you’re curious, you can go back through the change history to see some earlier takes on the problem.
Like it? Hate it? Got suggestions and improvements? (I always mess something up, so there’s a pretty much 100% chance there’s room for improvement.) Drop a note, a tweet, an email, or a comment.
Thanks to Ian Keen, who suggested extending OptionSet
directly.
Comments are closed.