A native option set construct


(Dan Stenmark) #1

One of the things that surprised me is that there still isn’t concise syntax for creating option sets, a pattern I see out in the wild a fair bit. While Swift 2 did introduce OptionSetType for structs, it still feels somewhat obtuse and non-obvious. It would be great if we could something like:

options NotSoSecretVariations {
  case ProteinStyle,
  case AnimalStyle,
  case GrilledOnions,
  ...
}

That said, I have a feeling this isn’t the first time the Swift team has come across a pitch like this. If this really is unfeasible, it would be great and really educational to hear what the challenges are.

Dan


(Chris Lattner) #2

We’ve definitely discussed it, at the same time we were designing the swift 2 era syntax.

The consensus from those discussions is that option sets are set-like and sets don’t/shouldn't have privileged declaration syntax. Instead of adding complexity to the core language for this, we’d rather eventually cover this with a hygienic macro system some day. That would allow option sets to remain a library feature.

-Chris

···

On Dec 21, 2015, at 1:08 PM, Dan Stenmark via swift-evolution <swift-evolution@swift.org> wrote:

One of the things that surprised me is that there still isn’t concise syntax for creating option sets, a pattern I see out in the wild a fair bit. While Swift 2 did introduce OptionSetType for structs, it still feels somewhat obtuse and non-obvious. It would be great if we could something like:

options NotSoSecretVariations {
  case ProteinStyle,
  case AnimalStyle,
  case GrilledOnions,
  ...
}

That said, I have a feeling this isn’t the first time the Swift team has come across a pitch like this. If this really is unfeasible, it would be great and really educational to hear what the challenges are.


(Brent Royal-Gordon) #3

One of the things that surprised me is that there still isn’t concise syntax for creating option sets, a pattern I see out in the wild a fair bit. While Swift 2 did introduce OptionSetType for structs, it still feels somewhat obtuse and non-obvious. It would be great if we could something like:

options NotSoSecretVariations {
  case ProteinStyle,
  case AnimalStyle,
  case GrilledOnions,
  ...
}

That said, I have a feeling this isn’t the first time the Swift team has come across a pitch like this. If this really is unfeasible, it would be great and really educational to hear what the challenges are.

Personally, what I'd like to see is the concept of an option separated from that of an option *set*. Once you do that, you can define an individual option simply using an enum, and let the set handle combining the options together.

Here's what I mean. Take an Objective-C NS_OPTIONS enum:

  typedef NS_OPTIONS(NSUInteger, NSStringCompareOptions) {
      NSCaseInsensitiveSearch = 1,
      NSLiteralSearch = 2, /* Exact character-by-character equivalence */
      NSBackwardsSearch = 4, /* Search from end of source string */
      NSAnchoredSearch = 8, /* Search is limited to start (or end, if NSBackwardsSearch) of source string */
      NSNumericSearch = 64, /* Added in 10.2; Numbers within strings are compared using numeric value, that is, Foo2.txt < Foo7.txt < Foo25.txt; only applies to compare methods, not find */
      NSDiacriticInsensitiveSearch NS_ENUM_AVAILABLE(10_5, 2_0) = 128, /* If specified, ignores diacritics (o-umlaut == o) */
      NSWidthInsensitiveSearch NS_ENUM_AVAILABLE(10_5, 2_0) = 256, /* If specified, ignores width differences ('a' == UFF41) */
      NSForcedOrderingSearch NS_ENUM_AVAILABLE(10_5, 2_0) = 512, /* If specified, comparisons are forced to return either NSOrderedAscending or NSOrderedDescending if the strings are equivalent but not strictly equal, for stability when sorting (e.g. "aaa" > "AAA" with NSCaseInsensitiveSearch specified) */
      NSRegularExpressionSearch NS_ENUM_AVAILABLE(10_7, 3_2) = 1024 /* Applies to rangeOfString:..., stringByReplacingOccurrencesOfString:..., and replaceOccurrencesOfString:... methods only; the search string is treated as an ICU-compatible regular expression; if set, no other options can apply except NSCaseInsensitiveSearch and NSAnchoredSearch */
  };

I think this should be translated to Swift like this:

  enum NSStringCompareOption: Int {
      case CaseInsensitiveSearch = 0 // Because 1 << 0 == 1
      case LiteralSearch = 1 // 1 << 1 == 2
      case BackwardsSearch = 2
      case AnchoredSearch = 3
      case NumericSearch = 6 // 1 << 6 == 64
      case DiacriticInsensitiveSearch = 7
      case WidthInsensitiveSearch = 8
      case ForcedOrderingSearch = 9
      case RegularExpressionSearch = 10
  }

Meanwhile, a method taking NSStringCompareOptions like this:

  - (NSComparisonResult)compare:(NSString *)string options:(NSStringCompareOptions)mask;

Should be translated as:

  func compare(string: NSString, options mask: OptionSet<NSStringCompareOption>) -> NSComparisonResult

OptionSet would be a generic type like this:

  struct OptionSet<Option: RawRepresentable where Option.RawValue: IntegerType>: SetAlgebraType, RawRepresentable {
      typealias Element = Option
      typealias RawValue = Option.RawValue
      ...
  }

This definition should work for any NS_OPTIONS where the options are non-overlapping. I'm not sure if that's *every* option set in the Apple frameworks, but I think it's the vast majority of them. (The incompatible ones could use the existing mechanism.) And to create your own option set, all you have to do is write an enum with an integer raw type that has less than 32 or 64 cases, depending on your target. You don't even have to specify the raw values—the default ones will work fine.

···

--
Brent Royal-Gordon
Architechies


(Joe Groff) #4

One reason we preserved the NS_OPTIONS model, where single options share the same type as option sets, is that it allows for API evolution. A library can break a single option down into multiple refined options while still offering the original option as the union of the new ones. So you can go from:

public struct LaundryOptions: OptionSetType {
  public static let EnergyStar = 1
}

to:

public struct LaundryOptions: OptionSetType {
  public static let EnergyStar: LaundryOptions = [.LowWater, .LowHeat]
  public static let LowWater = 1, LowHeat = 2
}

without breaking client code (or at least, client code that doesn't attempt to inspect the option set itself).

-Joe

···

On Dec 21, 2015, at 2:38 PM, Brent Royal-Gordon via swift-evolution <swift-evolution@swift.org> wrote:

One of the things that surprised me is that there still isn’t concise syntax for creating option sets, a pattern I see out in the wild a fair bit. While Swift 2 did introduce OptionSetType for structs, it still feels somewhat obtuse and non-obvious. It would be great if we could something like:

options NotSoSecretVariations {
  case ProteinStyle,
  case AnimalStyle,
  case GrilledOnions,
  ...
}

That said, I have a feeling this isn’t the first time the Swift team has come across a pitch like this. If this really is unfeasible, it would be great and really educational to hear what the challenges are.

Personally, what I'd like to see is the concept of an option separated from that of an option *set*. Once you do that, you can define an individual option simply using an enum, and let the set handle combining the options together.

Here's what I mean. Take an Objective-C NS_OPTIONS enum:

  typedef NS_OPTIONS(NSUInteger, NSStringCompareOptions) {
      NSCaseInsensitiveSearch = 1,
      NSLiteralSearch = 2, /* Exact character-by-character equivalence */
      NSBackwardsSearch = 4, /* Search from end of source string */
      NSAnchoredSearch = 8, /* Search is limited to start (or end, if NSBackwardsSearch) of source string */
      NSNumericSearch = 64, /* Added in 10.2; Numbers within strings are compared using numeric value, that is, Foo2.txt < Foo7.txt < Foo25.txt; only applies to compare methods, not find */
      NSDiacriticInsensitiveSearch NS_ENUM_AVAILABLE(10_5, 2_0) = 128, /* If specified, ignores diacritics (o-umlaut == o) */
      NSWidthInsensitiveSearch NS_ENUM_AVAILABLE(10_5, 2_0) = 256, /* If specified, ignores width differences ('a' == UFF41) */
      NSForcedOrderingSearch NS_ENUM_AVAILABLE(10_5, 2_0) = 512, /* If specified, comparisons are forced to return either NSOrderedAscending or NSOrderedDescending if the strings are equivalent but not strictly equal, for stability when sorting (e.g. "aaa" > "AAA" with NSCaseInsensitiveSearch specified) */
      NSRegularExpressionSearch NS_ENUM_AVAILABLE(10_7, 3_2) = 1024 /* Applies to rangeOfString:..., stringByReplacingOccurrencesOfString:..., and replaceOccurrencesOfString:... methods only; the search string is treated as an ICU-compatible regular expression; if set, no other options can apply except NSCaseInsensitiveSearch and NSAnchoredSearch */
  };

I think this should be translated to Swift like this:

  enum NSStringCompareOption: Int {
      case CaseInsensitiveSearch = 0 // Because 1 << 0 == 1
      case LiteralSearch = 1 // 1 << 1 == 2
      case BackwardsSearch = 2
      case AnchoredSearch = 3
      case NumericSearch = 6 // 1 << 6 == 64
      case DiacriticInsensitiveSearch = 7
      case WidthInsensitiveSearch = 8
      case ForcedOrderingSearch = 9
      case RegularExpressionSearch = 10
  }

Meanwhile, a method taking NSStringCompareOptions like this:

  - (NSComparisonResult)compare:(NSString *)string options:(NSStringCompareOptions)mask;

Should be translated as:

  func compare(string: NSString, options mask: OptionSet<NSStringCompareOption>) -> NSComparisonResult

OptionSet would be a generic type like this:

  struct OptionSet<Option: RawRepresentable where Option.RawValue: IntegerType>: SetAlgebraType, RawRepresentable {
      typealias Element = Option
      typealias RawValue = Option.RawValue
      ...
  }

This definition should work for any NS_OPTIONS where the options are non-overlapping. I'm not sure if that's *every* option set in the Apple frameworks, but I think it's the vast majority of them. (The incompatible ones could use the existing mechanism.) And to create your own option set, all you have to do is write an enum with an integer raw type that has less than 32 or 64 cases, depending on your target. You don't even have to specify the raw values—the default ones will work fine.