OptionSetType and enums
Asked Answered
M

3

20

I have an enum named ProgrammingLanguage:

enum ProgrammingLanguage {
  case Swift, Haskell, Scala
}

Now I have a class named Programmer with the following property:

let favouriteLanguages: ProgrammingLanguage = .Swift

Seeing how a programmer could have several favourite languages, I'd thought it'd be nice to write something like this:

let favouriteLanguages: ProgrammingLanguage = [.Swift, .Haskell]

After a bit of research, I realized that I need to conform to OptionSetType, but in doing so, I've raise the following 3 errors:

ProgrammingLanguage does not conform to

  1. SetAlgebraType
  2. OptionSetType
  3. RawRepresentable

When I saw the Raw Representable error, I immediately thought of associated types for enums. I wanted to be able to print the enum value anyway, so I changed my enum signature to the following:

case ProgrammingLanguage: String, OptionSetType {
  case Swift, Haskell, Scala
}

This silenced 2 of the warnings. But I'm still left with one which is that I don't conform to protocol SetAlgebraType.

After a bit of trial and error, I found out having the associated type of the enum as Int fixed it (which makes sense, since the RawRepresentable protocol requires you to implement an initializer of the signature init(rawValue: Int)). However, I'm unsatisfied with that; I want to be able to get the String representation of the enum easily.

Could someone advise me how I can do this easily, and why OptionSetType requires an Int associated type?

Edit:

The following declaration compiles correctly, but errors at runtime:

enum ProgrammingLanguage: Int, OptionSetType {
  case Swift, Scala, Haskell
}

extension ProgrammingLanguage {
  init(rawValue: Int) {
    self.init(rawValue: rawValue)
  }
}

let programmingLanguages: ProgrammingLanguage = [.Swift, .Scala]
Maurya answered 24/4, 2016 at 4:3 Comment(5)
I doubt that you can combine enum and OptionSetType because both rawValue's interfere with each other.Gourde
@Gourde It's really interesting that it compiles correctly. When running, the app crashes on the line that tries to init the option set, with the debugger showing the init called 31970 times before crashing.Maurya
I guess the code runs into an infinite loop (which doesn't matter at compile time) or something similar and then crashes on a buffer overflow.Gourde
I believe that you are confusing associated values with raw values: developer.apple.com/library/content/documentation/Swift/…Sandal
related twitter.com/johnsundell/status/906097785883242496Karriekarry
H
19

Edit: I'm surprised at my former self for not saying this upfront at the time, but... instead of trying to force other value types into the OptionSet protocol (Swift 3 removed Type from the name), it's probably better to consider the API where you use those types and use Set collections where appropriate.

OptionSet types are weird. They are both collections and not collections — you can construct one from multiple flags, but the result is still a single value. (You can do some work to figure out a collection-of-single-flags equivalent to such a value, but depending on the possible values in the type it might not be unique.)

On the other hand, being able to have one something, or more than one unique somethings, can be important to the design of an API. Do you want users to say they have more than one favorite, or enforce that there's only one? Just how many "favorites" do you want to allow? If a user claims multiple favorites, should they be ranked in user-specific order? These are all questions that are hard to answer in an OptionSet-style type, but much easier if you use a Set type or other actual collection.

The rest of this answer a) is old, using Swift 2 names, and b) assumes that you're trying to implement OptionSet anyway, even if it's a bad choice for your API...


See the docs for OptionSetType:

Supplies convenient conformance to SetAlgebraType for any type whose RawValue is a BitwiseOperationsType.

In other words, you can declare OptionSetType conformance for any type that also adopts RawRepresentable. However, you gain the magic set-algebra syntax support (via operators and ArrayLiteralConvertible conformance) if and only if your associated raw value type is one that conforms to BitwiseOperationsType.

So, if your raw value type is String, you're out of luck — you don't gain the set algebra stuff because String doesn't support bitwise operations. (The "fun" thing here, if you can call it that, is that you can extend String to support BitwiseOperationsType, and if your implementation satisfies the axioms, you can use strings as raw values for an option set.)

Your second syntax errors at runtime because you've created an infinite recursion — calling self.init(rawValue:) from init(rawValue:) keeps gong until it blows the stack.

It's arguably a bug (please file it) that you can even try that without a compile time error. Enums shouldn't be able to declare OptionSetType conformance, because:

  1. The semantic contract of an enum is that it's a closed set. By declaring your ProgrammingLanguage enum you're saying that a value of type ProgrammingLanguage must be one of Swift, Scala, or Haskell, and not anything else. A value of "Swift and Scala" isn't in that set.

  2. The underlying implementation of an OptionSetType is based on integer bitfields. A "Swift and Haskell" value, ([.Swift, .Haskell]) is really just .Swift.rawValue | .Haskell.rawValue. This causes trouble if your set of raw values isn't bit-aligned. That is, if .Swift.rawValue == 1 == 0b01, and .Haskell.rawValue == 2 == 0b10, the bitwise-or of those is 0b11 == 3, which is the same as .Scala.rawValue.

TLDR: if you want OptionSetType conformance, declare a struct.

And use static let to declare members of your type.

And pick your raw values such that members you want to be distinct from possible (bitwise-or) combinations of other members actually are.

struct ProgrammingLanguage: OptionSetType {
    let rawValue: Int

    // this initializer is required, but it's also automatically
    // synthesized if `rawValue` is the only member, so writing it
    // here is optional:
    init(rawValue: Int) { self.rawValue = rawValue }

    static let Swift    = ProgrammingLanguage(rawValue: 0b001)
    static let Haskell  = ProgrammingLanguage(rawValue: 0b010)
    static let Scala    = ProgrammingLanguage(rawValue: 0b100)
}

Good ways to keep your values distinct: use binary-literal syntax as above, or declare your values with bit shifts of one, as below:

    static let Swift    = ProgrammingLanguage(rawValue: 1 << 0)
    static let Haskell  = ProgrammingLanguage(rawValue: 1 << 1)
    static let Scala    = ProgrammingLanguage(rawValue: 1 << 2)
Holoblastic answered 25/4, 2016 at 22:15 Comment(4)
Minor remark: There seems to be a default implementation for init(rawValue: ), it can be omitted here.Rationale
@MartinR: Yes, you get init(rawValue:) for free iff your only instance property is rawValue. But if you add more properties, you lose protocol conformances that depend on there being an initializer that's exactly init(rawValue:). I find it helpful to put it there anyway — it guards against such breaking changes in the future, and it makes the protocol conformance more explicit.Holoblastic
Are you aware of any solution for "extend String to support BitwiseOperationsType" It would make life a lot easier...Karriekarry
If you really want to go down that route, see the docs I linked to for the BitwiseOperations protocol (Swift 3 dropped Type from the name). You need a ^ operator that takes two strings and returns a value that can be treated as the symmetric difference between them; e.g. a ^ a == .allZeroes and a ^ .allZeroes == a. And so on for the other operators.Holoblastic
R
12

I guess you could simply achieve it in the modern way {^_^}.

protocol Option: RawRepresentable, Hashable, CaseIterable {}

extension Set where Element: Option {
    var rawValue: Int {
        var rawValue = 0
        for (index, element) in Element.allCases.enumerated() where contains(element) {
            rawValue |= (1 << index)
        }
        return rawValue
    }
}

... then

enum ProgrammingLanguage: String, Option {
    case Swift, Haskell, Scala
}
typealias ProgrammingLanguages = Set<ProgrammingLanguage>

let programmingLanguages: ProgrammingLanguages = [.Swift, .Haskell]

Reference: https://nshipster.com/optionset/

Racketeer answered 23/8, 2019 at 15:33 Comment(1)
wow you are genius. You made my day! Thank you so much!Serious
G
0

You can just use the approach made in this mini-library: https://github.com/allexks/Options

This way all you need to do is make your enum conform to CaseIterable and then you can simply write:

let favouriteLanguages: Options<ProgrammingLanguage> = [.Swift, .Haskell]
Galliwasp answered 31/8, 2021 at 18:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.