orphan: |
---|
SUMMARY: Option sets should be structs of Bools, with a protocol to provide bitwise-ish operations.
Option sets in C and ObjC are often represented using enums with bit-pattern constants, as used in Cocoa's NS_OPTIONS idiom. For example:
// ObjC typedef NS_OPTIONS(NSUInteger, NSStringCompareOptions) { NSCaseInsensitiveSearch = 1, NSLiteralSearch = 2, NSBackwardsSearch = 4, NSAnchoredSearch = 8, NSNumericSearch = 64, NSDiacriticInsensitiveSearch = 128, NSWidthInsensitiveSearch = 256, NSForcedOrderingSearch = 512, NSRegularExpressionSearch = 1024 };
This approach doesn't map well to Swift's enums, which are intended to be strict enumerations of states, or "sum types" to use the type-theory-nerd term. An option set is more like a product type, and so more naturally map to a struct of booleans:
// Swift struct NSStringCompareOptions { var CaseInsensitiveSearch, LiteralSearch, BackwardsSearch, AnchoredSearch, NumericSearch, DiacriticInsensitiveSearch, WidthInsensitiveSearch, ForcedOrderingSearch, RegularExpressionSearch : Bool = false }
There are a few reasons this doesn't fly in C:
Boolean fields in C structs waste a byte by default. Option set enums are compact.
Bitfield ABI has historically been weird and unstable across C implementations. Option set enums have a very concrete binary representation.
Prior to C99 it was difficult to use struct literals in expressions.
It's useful to apply bitwise operations to option sets, which can't be applied to C structs.
Bitmasks also provide a natural way to express common option subsets as constants, as in the
AllEdges
constants in the following example:// ObjC typedef NS_OPTIONS(unsigned long long, NSAlignmentOptions) { NSAlignMinXInward = 1ULL << 0, NSAlignMinYInward = 1ULL << 1, NSAlignMaxXInward = 1ULL << 2, NSAlignMaxYInward = 1ULL << 3, NSAlignWidthInward = 1ULL << 4, NSAlignHeightInward = 1ULL << 5, NSAlignMinXOutward = 1ULL << 8, NSAlignMinYOutward = 1ULL << 9, NSAlignMaxXOutward = 1ULL << 10, NSAlignMaxYOutward = 1ULL << 11, NSAlignWidthOutward = 1ULL << 12, NSAlignHeightOutward = 1ULL << 13, NSAlignMinXNearest = 1ULL << 16, NSAlignMinYNearest = 1ULL << 17, NSAlignMaxXNearest = 1ULL << 18, NSAlignMaxYNearest = 1ULL << 19, NSAlignWidthNearest = 1ULL << 20, NSAlignHeightNearest = 1ULL << 21, NSAlignRectFlipped = 1ULL << 63, // pass this if the rect is in a flipped coordinate system. This allows 0.5 to be treated in a visually consistent way. // convenience combinations NSAlignAllEdgesInward = NSAlignMinXInward|NSAlignMaxXInward|NSAlignMinYInward|NSAlignMaxYInward, NSAlignAllEdgesOutward = NSAlignMinXOutward|NSAlignMaxXOutward|NSAlignMinYOutward|NSAlignMaxYOutward, NSAlignAllEdgesNearest = NSAlignMinXNearest|NSAlignMaxXNearest|NSAlignMinYNearest|NSAlignMaxYNearest, };
However, we can address all of these issues in Swift. We should make the theoretically correct struct-of-Bools representation also be the natural and optimal way to express option sets.
One of the key features of option set enums is that, by using the standard C bitwise operations, they provide easy and expressive intersection, union, and negation of option sets. We can encapsulate these capabilities into a protocol:
// Swift protocol OptionSet : Equatable { // Set intersection @infix func &(_:Self, _:Self) -> Self @infix func &=(_: inout Self, _:Self) // Set union @infix func |(_:Self, _:Self) -> Self @infix func |=(_: inout Self, _:Self) // Set xor @infix func ^(_:Self, _:Self) -> Self @infix func ^=(_: inout Self, _:Self) // Set negation @prefix func ~(_:Self) -> Self // Are any options set? func any() -> Bool // Are all options set? func all() -> Bool // Are no options set? func none() -> Bool }
The compiler can derive a default conformance for a struct whose instance stored
properties are all Bool
:
// Swift struct NSStringCompareOptions : OptionSet { var CaseInsensitiveSearch, LiteralSearch, BackwardsSearch, AnchoredSearch, NumericSearch, DiacriticInsensitiveSearch, WidthInsensitiveSearch, ForcedOrderingSearch, RegularExpressionSearch : Bool = false } var a = NSStringCompareOptions(CaseInsensitiveSearch: true, BackwardsSearch: true) var b = NSStringCompareOptions(WidthInsensitiveSearch: true, BackwardsSearch: true) var c = a & b (a & b).any() // => true c == NSStringCompareOptions(BackwardsSearch: true) // => true
Boolean fields should take up a single bit inside aggregates, avoiding the need to mess with bitfields to get efficient layout. When used as inout arguments, boolean fields packed into bits can go through writeback buffers.
Option subsets can be expressed as static functions of the type. (Ideally these would be static constants, if we had those.) For example:
// Swift struct NSAlignmentOptions : OptionSet { var AlignMinXInward, AlignMinYInward, AlignMaxXInward, AlignMaxYInward, AlignWidthInward, AlignHeightInward : Bool = false // convenience combinations static func NSAlignAllEdgesInward() { return NSAlignmentOptions(AlignMinXInward: true, AlignMaxXInward: true, AlignMinYInward: true, AlignMaxYInward: true) } }
When importing an NS_OPTIONS declaration from Cocoa, we import it as an
OptionSet-conforming struct, with each single-bit member of the Cocoa enum
mapping to a Bool field of the struct with a default value of false
.
Their IR-level layout places the fields
at the correct bits to be ABI-compatible with the C type.
Multiple-bit constants are imported as option subsets, mapping to static
functions.
OPEN QUESTION: What to do with bits that only appear as parts of option subsets, as in:
// ObjC typedef NS_OPTIONS(unsigned, MyOptions) { Foo = 0x01, Bar = 0x03, // 0x02 | 0x01 Bas = 0x05, // 0x04 | 0x01 };
There are some things that are a bit awkward under this proposal which I think are worthy of some examination. I don't have great solutions to any of these issues off the top of my head.
It's a bit boilerplate-ish to have to spell out the : Bool = true
for the
set of fields:
// Swift struct MyOptions : OptionSet { var Foo, Bar, Bas : Bool = false }
(though by comparison with C, it's still a net win, since the bitshifted constants don't need to be manually spelled out and maintained. Is this a big deal?)
The implicit elementwise keyworded constructor for structs works naturally for
option set structs, except that it requires a bulky and repetitive : true
(or : false
) after each keyword:
// Swift var myOptions = MyOptions(Foo: true, Bar: true)
Some sort of shorthand for keyword: true
/keyword: false
would be nice
and would be generally useful beyond option sets, though I don't have any
awesome ideas of how that should look right now.
Treating individual options and option subsets differently disrupts some
of the elegance of the bitmask idiom. As static functions, option subsets can't
be combined freely in constructor calls like they can with |
in C. As
instance stored properties, individual options must be first constructed before
bitwise operations can be applied to them.
// ObjC typedef NS_OPTIONS(unsigned, MyOptions) { Foo = 0x01, Bar = 0x02, Bas = 0x04, Foobar = 0x03, }; MyOptions x = Foobar | Bas;
// Swift, under this proposal struct MyOptions : OptionSet { var Foo, Bar, Bas : Bool = false static func Foobar() -> MyOptions { return MyOptions(Foo: true, Bar: true) } } var x: MyOptions = .Foobar() | MyOptions(Bas: true)
This nonuniformity could potentially be addressed by introducing additional implicit decls, such as adding implicit static properties corresponding to each individual option:
// Swift struct MyOptions : OptionSet { // Stored properties of instances var Foo, Bar, Bas : Bool = false static func Foobar() -> MyOptions { return MyOptions(Foo: true, Bar: true) } // Implicitly-generated static properties? static func Foo() -> MyOptions { return MyOptions(Foo: true) } static func Bar() -> MyOptions { return MyOptions(Bar: true) } static func Bas() -> MyOptions { return MyOptions(Bas: true) } } var x: MyOptions = .Foobar() | .Bas()
This is getting outside of strict protocol conformance derivation, though.
Static constant properties seem to me like a necessity to make option subsets really acceptable to declare and use. This would be a much nicer form of the above:
// Swift struct MyOptions : OptionSet { // Stored properties of instances var Foo, Bar, Bas : Bool = false static val Foobar = MyOptions(Foo: true, Bar: true) // Implicitly-generated static properties static val Foo = MyOptions(Foo: true) static val Bar = MyOptions(Bar: true) static val Bas = MyOptions(Bas: true) } var x: MyOptions = .Foobar | .Bas