I like this proposal — defining OptionSets is something that must be done from time to time and the boilerplate is annoying. Macros are a good fit for this functionality. I much prefer the struct version for the reasons @Joe_Groff mentioned.
I’d clarify the functionality of the struct version though: the original pitch allowed us to explicitly choose which options corresponded to which bits (nextDay is bit 0, secondDay is bit 1, nobody use bit 2, priority is bit 3, etc.) by specifying a raw value on the Options enum. Is this possible using your alternative implementation? Maybe I missed this, but that sort of thing can be very useful when you need it.
Maybe just by specifying a default value in the static vars?
@OptionSet<UInt8>
struct ShippingOptions {
static var nextDay: ShippingOptions = 0
static var secondDay: ShippingOptions = 1
// Nobody use bit 2 please
static var priority: ShippingOptions = 3
static var standard: ShippingOptions = 4
static let express: ShippingOptions = [.nextDay, .secondDay]
static let all: ShippingOptions = [.express, .priority, .standard]
}
Can someone make the case for me that option sets should not just be packed structs with a bunch of defaulted stored properties? My sense is that both the nested Option type and all the set algebra are generally just used to get niftier syntax for constructing and amending values in fairly concrete ways, like turning specific options on or off. If that's true, then the whole feature feels like a C-ism to me that we shouldn't be perpetuating, and it would be better for us to put effort into improving this kind of aggregate initialization / amendment for structs in general, e.g. by adding a syntax that desugars into calling a series of setters on a default-constructed value (or a similar syntax to amend an r-value). And packing boolean properties into bit fields is certainly something that we could just be doing in the compiler.
It's hard not to notice that a lot of these examples feel forced into this set-of-flags model. For example, the choice between next-day / second-day / priority / standard shipping is really an enumerated choice: you have to pick exactly one. A representation that pretends otherwise is a bad representation.
I thought OptionSet was always justified as a nicer representation of NS_OPTIONS from Obj-C. So they'll be around for as long as that bridge is necessary. Given the right syntax, this macro seems like an easy win. Perhaps the macro could enable better Swift syntax instead? That would still need changes to the bridge.
A realistic case of this came up during the review of SE-0211: Unicode Scalar Properties. Option sets would have been an interesting API to explore, since that would have mapped nicely to the ICU flag sets for those scalars, but we would have been limited to 64 Boolean properties and IIRC ICU was already exceeding that at the time, or was close to it. So instead, we wrote a bunch of computed properties by hand. (Although, I think another reason it wasn't feasible was because all the ICU properties would have to have been queried individually to form the bit set, so it may have not been a good fit anyway.)
A more Swift-native "set of flags" type that wasn't restricted by the limitations of an explicit raw integer type would certainly be useful.
I can definitely see the argument that this is useful in interop cases, but in interop cases you presumably also need to modify an existing type, not add one from scratch. Having a macro that can introspect an imported type to generate some boilerplate in an extension would be very useful, but that's beyond the currently-proposed capabilities of macros, and I doubt it would be the same macro as this anyway.
Part of my argument here is that the Swift-native "set of flags" concept is not useful. Configuration types are certainly important, and I think having better syntax to make them easy to initialize would be great. But as that configuration expands, it's very likely to grow in ways that fundamentally conflict with the "set of flags" idea, which starts dragging the whole thing back and forcing configuration to be split into the parts that suit a set of flags and the parts that don't. So I feel pretty strongly that it's better to make a simple struct the right tool for the job.
I find it's often easier to reason with that than set algebra. So if a macro (or a language mechanism) was to add properties directly so the above can be rewriten as:
No need to go that far, but won't OptionSets be critical for the Foundation in Swift rewrite? So we'll need the capability to write them from Swift and have them appear to Obj-C exactly as if they'd been written there.
The reverse-interop use case is definitely interesting, thanks for reminding me of that. I think the current vision for the Foundation-in-Swift rewrite doesn't completely abandon ObjC headers for that kind of thing, though; e.g. see the pitch for implementing ObjC @interfaces in Swift.
you said "structs". Perhaps you could've picked tuples as well? Especially the special case of homogeneous tuple (should we have it one day).
we do not have packed structs yet (where Bool fields are packed as bits) - important for C interop.
with the struct approach (as well with some other solutions including OptionSet) you may have other unrelated stored variables in the struct. This is different to enum-like approach (you can't have unrelated stored variables).
contains([.several, .fields, .at, .once]) would be harder to do with a struct full of bools / bits
likewise intersection / union set operations would have to be implemented manually
with structs it is hard to express "gaps", e.g. bits 0, 1, 2, <gap here>, 63 - (would you create some dummy unused bits?). could be important for C interop.
on the bright side structs can represent more than 64 values.
switch exhaustivity might be a factor (doesn't currently work with structs – heck, not even with UInt8! – but works with tuples and enums).
I am not against C-ism per se (each feature should be weighted on its own merit) but structs with bits could be seen as another form of C-ism - bit fields.
I really have no idea about that, other than to say that just looks like a random post at the end of thread (I don't know that person's impact) and that the ability suggested there would seem rather antithetical to the rewrite's cross platform goals. If the Apple version of Foundation-in-Swift just calls into some Obj-C implementation, we still have two implementations to deal with, greatly undermining the rewrite's benefits.
I somehow missed this, thanks for reiterating. If this is the case, then I have to give a big -1 for this proposal. I don't think it's worth the expansion of the standard library for an addition to an API that still feels very awkward at its core, especially since this would be one of the first publicly vended macros in the standard library.
It would be options.several && options.fields && options.at && options.once. If that was a common grouping, you'd just make a computed property for it.
Absolutely, arbitrary set algebra would be much more difficult. That is why I asked whether anyone wants to do arbitrary set algebra on these types. It is important, when designing a feature, to understand how it is actually going to be used. My impression is that people almost exclusively use those operations with constant operands, which is a pattern that easily translates to setting properties on a struct. If I'm wrong about that, and there are important use patterns that require arbitrary set algebra, that would be very good to know.
None of the patterns proposed here enable exhaustive switch over an entire option set, and honestly I'm not even sure why you'd want to — that would usually be a very large set of cases. You can switch over a specific option, but that would only matter if you were doing something like iterating the current set of options. That would, indeed, be more difficult with a struct. So my question, again, is whether there is an important use pattern where people need to do that, or is it just something that happens to be enabled by the representation but is generally not useful? Because it doesn't make any sense to design a feature around capabilities that people don't use. Meanwhile, structs have a lot of capabilities that OptionSets do not, like being able to store things that aren't boolean flags, which make them much more powerful for the general purpose of expressing configuration.
The interop advantages of OptionSets are substantial. If this feature is designed largely for interop needs, that would also answer my question in a way. But I don't think we should encourage native configuration types to be written this way.
I'm not sure what you're talking about in this post. I linked to a thread proposing the ability to import an Objective-C header and then implement an @interface from it in Swift. The implementation is still in Swift, it's just that the Objective-C header continues to exist and remains a source of truth about the Objective-C interface. I don't know for certain whether the Foundation rewrite intends to make use of that, but it is a good way to maintain perfect API and ABI compatibility with an existing interface.
Hmm. On the one hand - yes, the compiler should be able to compact booleans in to bits and essentially generate equivalent code to an option-set.
On the other hand, we've been talking about those kinds of optimisations since Swift began. 8 years later, those basic layout optimisations are still unimplemented and do not appear to be a priority AFAICT:
struct Padded { var a: UInt8; var b: UInt64 }
print(MemoryLayout<Padded>.size) // 16 - could be 9
struct Bools { var a: Bool; var b: Bool; var c: Bool; var d: Bool; var e: Bool; var f: Bool; var g: Bool; var h: Bool; }
print(MemoryLayout<Bools>.size) // 8 - could be 1
Meanwhile, developers need these kinds of guarantees to engineer their data structures. For some designs, it is simply unacceptable for a structure containing 8 booleans to occupy 8 entire bytes (64 bits). Those developers either need to give up with Swift and use a different language for those data structures, or use something like OptionSet which essentially manually lays out flags in some fixed-size storage. For those who decide to persevere with Swift, we can at least use macros to lessen the pain.
I'm sure we'd all be happy if the compiler implemented layout optimisations, but we could easily be waiting another 8+ years for that. This is a minor shorthand over the existing facilities, could be deployed much more quickly, and is more reliable.
You mean OptionSet's set operations were added just in case and there was no real need for that? A big player like Apple can remove OptionSet's "set" operations like "union/formUnion/intersect/etc" locally and run a massive suit of real world sources to see how many things will break and where.
Switch exhaustivity could be useful for small sets. E.g. if I have a set of [disabled, highlighted] I'l like to exhaust all four states explicitly (which regretfully is currently not possible) without adding default case, as I may add a third case a bit later (e.g. "focused") and naturally forget to handle it due to having a default case.
As a side note, I don't completely appreciate the fact that while I can exhaust (Bool, Bool) I can't exhaust struct S { var x, y: Bool }
Sometimes "less is more". E.g. having a dedicated bit-set type opens up an opportunity to store fields as bits, while a more general struct type that allows both bits and other fields either doesn't have this feature from start or, as in C bit fields opens a nasty can of warms.
Yes, for starters all those NS_OPTIONS / CF_OPTIONS found in UIKit, AVFoundation, ARKit, etc. Surprisingly, it's not a huge list across Apple frameworks, perhaps some 300 entries. But understandably, it's not only about Apple frameworks...
Wherever possible I've switched over the struct of bitfields approach in C++ and I think it's a significant improvement there over the traditional bitmasks even with some C++-specific problems with it. Swift would not have those problems, so it should be clearly better in nearly all non-interop scenarios. I've always seen the more advanced set algebra functionality on bitsets as a cute trick that's a nice consequence of bitsets being sets, but not really something actually valuable.
As Karl says though, these two options are very dissimilar in terms of implementation effort required. If someone is actively working on the relevant layout optimizations and it'll ship soon then I think it'd make sense to skip this macro, but as long as that remains a future dream this macro is a good stop-gap.
As the Swift API guidelines state, we should favour clarity over brevity. It is not immediately apparent one is declaring an OptionSet, but an enum inside a struct. I would argue that the bit shifting on static lets is more readable than an enum and make an optionset very quickly recognisable from normal structs. More so, the default Xcode colour scheme makes some @ markers easy to miss (property wrappers especially so). I don’t know how Xcode will eventually color macros but we can’t rely on any IDE to visually help discovery. It’s also yet another @ Why so many "@" expressions?
OptionSets are too rare to warrant a macro in the standard library. They’re a “niche” language feature if you will that is only used in environments where performance and storage are amazingly important - very low level programming, embedded systems etc. A large proportion of swift developers never create their own option sets. The ones that do, I'm not convinced they particularly need a macro for it in the standard library. Most run of the mill optionsets are not so large that they get hard to read.
The original proposal and all the variants in this thread don’t shorten the code needed to write an optionsset, some of them enlarge it
Addition to the standard library checklist:
Does this solve a common problem?
No
Does it make code more readable?
No
Is this flexible enough?
Not in the original form, but the thread exposed some possibilities for flexibility however those made the code even more unreadable
Does it help write correct code?
Yes
Can it avoid performance problems?
--
All in all I don’t think the macro is so useful that it needs to be part of the standard library and it is very clear that addition of a new type or keyword is undesirable. I personally don’t understand why we can’t have some syntactic sugar for this the same way Int? is syntactic sugar for Optional<Int>. If the private enum could have been written as “options x, y, z” just as pure syntactic sugar then perhaps this would be a lot more readable
One idea for the syntax - I recall a few years ago hearing about metaclasses for C++:
Provide a new abstraction authoring mechanism so programmers can write new kinds of user-defined abstractions that encapsulate behavior. In current C++, the function and the class are the two mechanisms that encapsulate user-defined behavior. In this paper, $class metaclasses enable defining categories of classes that have common defaults and generated functions, and formally expand C++’s type abstraction vocabulary beyond class/struct/union/enum.
For instance, you could define an interface metaclass, which would use compile-time code to ensure all methods are pure virtual and that there are no data members, it would also generate a pure virtual destructor, etc. Then you could use it as a declaration kind, like this:
interface drawable { // this is an interface
int draw(canvas& c); // draw now defaults to public pure virtual
// ...
};
Maybe that's a direction worth exploring for macros - so we could actually have something like:
optionset Flags: UInt8 {
var a, b, c, d
}
And the macro would generate the appropriate struct, as a library feature.
I could see that kind of system having other uses, as well. For instance, SwiftUI views are not really like most structs because their properties usually have reference semantics via @State and @Binding, and the need to annotate all of those properties leads to views becoming very noisy. Perhaps they should be able to define a new kind of declaration via a macro:
view Map {
var region: MKCoordinateRegion = ...
}
// would generate the same code as you write manually today:
struct Map: View {
@State var region: MKCoordinateRegion = ...
}
// But it would eliminiate a lot of noise:
// - 'struct' becomes an implementation detail
// - conformance to 'View' is implicit
// - @State becomes the default
I too have this extension, except that I have it as an extension of SetAlgebra, because that lets me use it on Set<Foo> values too.
IMO, this extension underscores the point that there is nothing useful about OptionSet beyond its ability to represent its values as a bitmask, which is most commonly (albeit not universally) useful when interoperating with C-based APIs.