Search code examples
swiftgenericsbit-fieldsswift3bitvector

Converting array of bit indexes to OptionSet


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.


Solution

  • 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.