2

I'm trying to write a helper function which will convert an array of bit indexes to a class conforming to OptionSet.

func getOptionSet<T: OptionSet>(bitIndexes: [Int64]) -> T {
    var result: Int64 = 0
    for index in bitIndexes {
        result |= 1 << index
    }
    return T(rawValue: result) // error
}

This fails to compile:

Cannot invoke initializer for type 'T' with an argument list of type '(rawValue: Int64)'

I've also tried using RawValue:

func getOptionSet<T: OptionSet>(bitIndexes: [T.RawValue]) {
    var result = T.RawValue()  // error

This doesn't work as well:

Cannot invoke value of type 'T.RawValue.Type' with argument list '()'

Can this be done? Do I need to add additional constraints on T?

I know it's possible to rewrite this function to use a concrete type, but I want to keep it generic if possible.

Zmey
  • 2,304
  • 1
  • 24
  • 40

2 Answers2

5

The problem in your code is that Int64 and T.RawValue are unrelated and can be different types.

But every unsigned integer type can be converted from and to UIntMax, so the problem can be solved by restricting RawValue to UnsignedInteger.

Using @OOPer's idea to define a custom initializer this would be:

extension OptionSet where RawValue: UnsignedInteger {
    init(bitIndexes: [Int]) {
        var result: UIntMax = 0
        for index in bitIndexes {
            result |= 1 << UIntMax(index)
        }
        self.init(rawValue: RawValue(result))
    }
}

which can also be written as

extension OptionSet where RawValue: UnsignedInteger {
    init(bitIndexes: [Int]) {
        let result = bitIndexes.reduce(UIntMax(0)) {
            $0 | 1 << UIntMax($1)
        }
        self.init(rawValue: RawValue(result))
    }
}

All option set types that I have seen so far have an unsigned integer type as raw value, but note that the same would also work with SignedInteger and IntMax.

Example:

struct TestSet: OptionSet {
    let rawValue: UInt16
    init(rawValue: UInt16) {
        self.rawValue = rawValue
    }
}

let ts = TestSet(bitIndexes: [1, 4])
print(ts) // TestSet(rawValue: 18)

Compare also How do you enumerate OptionSetType in Swift? for the reverse task.


Update: As of Swift 4 the UnsignedInteger protocol has a

public static func << <RHS>(lhs: Self, rhs: RHS) -> Self where RHS : BinaryInteger

method, so that the above code can be simplified to

extension OptionSet where RawValue: UnsignedInteger {
    init(bitIndexes: [Int]) {
        self.init(rawValue: bitIndexes.reduce(0) { $0 | 1 << $1 })
    }
}

without intermediate conversion to a “maximal” integer type.

Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Thank you, this is very useful. Is it worth casting everything to UIntMax for 32 bit platforms or is that a non issue? let result: UIntMax = bitIndexes.reduce(UIntMax(0)) { UIntMax($0) | UIntMax(1) << UIntMax($1) } – Zmey Jul 15 '16 at 15:15
  • 1
    @Zmey:The OptionSet type can have a 64-bit RawValue even on 32-bit platforms. If the intermediate value is 32-bit only then you lose bits.  But you don't need `UIntMax($0)` or `UIntMax(1)`, those types are inferred automatically. – Actually I made that wrong in between as you can see from the edit history. It should be correct now. – Martin R Jul 15 '16 at 15:17
  • 1
    @Zmey: The intermediate result/accumulator could also have the type RawValue. The problem is that the left-shift operator `<<` is not defined in the (Un)SignedInteger protocol, so you would have to define additional protocol extensions for all possible raw types, which I tried to avoid. – In revision 1 of this answer I solved that by multiplication instead of left-shift. So it is possible, but I found this code more elegant. – Martin R Jul 15 '16 at 15:34
  • 1
    @Zmey: Things became simpler in Swift 4, see updated answer. – Martin R May 06 '18 at 09:50
4

You may need a little more setups to make your getOptionSet to work:

protocol OptionBitShiftable: IntegerLiteralConvertible {
    func << (lhs: Self, rhs: Self) -> Self
    func |= (lhs: inout Self, rhs: Self)
}
extension Int64: OptionBitShiftable {}
extension UInt64: OptionBitShiftable {}
//...

With those above, you can write your getOptionSet like this:

func getOptionSet<T: OptionSet where T.RawValue: OptionBitShiftable>(bitIndexes: [T.RawValue]) -> T {
    var result: T.RawValue = 0
    for index in bitIndexes {
        result |= 1 << index
    }
    return T(rawValue: result)
}

Usage:

struct MyOptionSet: OptionSet {
    var rawValue: Int64
    init(rawValue: Int64) {
        self.rawValue = rawValue
    }
}
let myOption: MyOptionSet = getOptionSet(bitIndexes: [1,2,3,5,7])
print(myOption.rawValue) //->174(=2+4+8+32+128)

Or, you can define an initializer like this:

extension OptionSet where RawValue: OptionBitShiftable {
    init(bitIndexes: [RawValue]) {
        var result: RawValue = 0
        for index in bitIndexes {
            result |= 1 << index
        }
        self.init(rawValue: result)
    }
}

Which you can use as:

let alsoMyOption = MyOptionSet(bitIndexes: [4, 6])
print(alsoMyOption.rawValue) //->80(=16+64)
OOPer
  • 47,149
  • 6
  • 107
  • 142
  • Thank you, this works great! @martin-r addition is also very useful for converting from array of Int-s. – Zmey Jul 15 '16 at 14:44
  • Sadly I can't accept both answers, I'm marking Martin's one because it doesn't require defining additional protocols, I went with it in the app. But I very much appreciate your help! – Zmey Jul 15 '16 at 16:20