Swift: Madness of Generic Integer

548 阅读4分钟
原文链接: blog.krzyzanowskim.com

Handling Integers with Swift is not an easiest task. Especially when I need generic function that can do some work on any kind of Integer.

There is 11 Integer types that can be considered as Integers:

  • Int8
  • UInt8
  • Int16
  • UInt16
  • Int32
  • UInt32
  • Int64
  • UInt64
  • Int
  • UInt
  • Bit (I'll skip this one)

most of them conforms to enormous number of protocols, for example this is protocol hierarchy for Int, 26 protocols.

thankfully, some of them are "empty" protocols, used to describe characteristic of the type for eg. Strideable, ForwardIndexType, RandomAccessIndexType, IntegerType, SignedIntegerType etc..

My goal here is is to create generic function that will handle any integer value and make some operation on it. In this case I want build function that get bytes and return integer value build with given bytes array of bytes:

let bytes:[Byte] = [0xFF, 0xFF, 0xFF, 0xFF]
let result:UInt32? = integerWithBytes(bytes)

function is named integerWithBytes(), and sample input values are

[0xFF, 0xFF, 0xFF, 0xFF]

4 bytes long value (32 bits) equal to 16777215, with following bits representation

More or less, what I want to archive can be done with old world NSData, -[NSData getBytes:]:

func integerWithBytes(bytes: [UInt8]) -> T {
    var i:T
    var data = NSData(bytes: bytes, length: bytes.count)
    data.getBytes(&i, length: sizeofValue(i));
    retrurn i
   }

but it's not a case here. I want to have it Swift way, without raw memory.

Let's play.

Generic function

Let's build a generic function now. From the hierarchy of protocols I found that I could use protocol IntegerType as denominator of integerness of given value.

Hierarchy

First, naive version of function with parameter that conforms to protocol IntegerType is as follow:

func integerWithBytes(bytes:[UInt8]) -> T? {
    if (bytes.count < sizeof(T)) {
        return nil
    }
    var i:T = 0
    for (var j = 0; j < sizeof(T); j++) {
        i = i | (bytes[j] << (j * 8)) // error!
    }
    return i
}

error: could not find an overload for '<<' that="" accepts="" the="" supplied="" arguments<="" mark=""> - because shift operator '<<' is="" defined="" for="" every="" integer="" separatly,="" but="" not="" my="" generic="" type="" T: IntegerType

func <<(lhs: UInt64, rhs: UInt64) -> UInt64
func <<(lhs: Int64, rhs: Int64) -> Int64
func <<(lhs: UInt, rhs: UInt) -> UInt
func <<(lhs: Int, rhs: Int) -> Int
func <<(lhs: Int32, rhs: Int32) -> Int32
func <<(lhs: UInt32, rhs: UInt32) -> UInt32
func <<(lhs: Int16, rhs: Int16) -> Int16
func <<(lhs: UInt16, rhs: UInt16) -> UInt16
func <<(lhs: Int8, rhs: Int8) -> Int8
func <<(lhs: UInt8, rhs: UInt8) -> UInt8

here is a first sign that probably there is no simple way to build generic function. Every type is represented separatly, and signed integer is separated from unsigned. According to returned error message I can't do shift operation '<<' on="" type="" described="" as="" T: IntegerType... but for my function I need return value of given type T and it have to be integer.

Since I know that Int is largest possible integer (Int32 or Int64), I think I can use it for my bitwise shift operation. Sequence is as follow: cast value at first to Int, then shift, and cast back to given type T. This could look like this:

let i:T = T(Int(bytes[j]) << Int(j * 8)) // error

error: could not find an overload for 'init' that accepts the supplied arguments - Oups, this one is because initializers are not defined by any protocol! I can find them on every single struct: Int, UInt, Int8, UInt8, etc... but not formalized as protocol. I assumed that this is "de facto protocol" and build one named GenericIntegerType

protocol GenericIntegerType: IntegerType {
      init(_ v: Int)
      init(_ v: UInt)
      init(_ v: Int8)
      init(_ v: UInt8)
      init(_ v: Int16)
      init(_ v: UInt16)
      init(_ v: Int32)
      init(_ v: UInt32)
      init(_ v: Int64)
      init(_ v: UInt64)
}

my function looks as follow:

func integerWithBytes(bytes:[UInt8]) -> T? {
    if (bytes.count < sizeof(T)) {
        return nil
    }
    var i:T = 0
    for (var j = 0; j < sizeof(T); j++) {
        i = i | T(Int(bytes[j]) << Int(j * 8)) // ok
    }
    return i
}

at that point I thought that problem is solved. I was wrong.

Sign matters

As soon as I started first tests, I found that it is not working for some types, see

let result:UInt32? = integerWithBytes(bytes) // ok
let result:Int32?  = integerWithBytes(bytes) // error
let result:UInt8?  = integerWithBytes(bytes) // ok 
let result:UInt64? = integerWithBytes(bytes) // error

obviously here is a room for improvements ;)

First of all... I need separate signed types, and unsigned ones! This is the major drawback. I can't (maybe you can) build single generic function.

Ok, so now I have two versions, one for unsigned integers:

func integerWithBytes(bytes:[UInt8]) -> T? {
    (...)
    var i:UIntMax = 0
    for (var j = 0; j < maxBytes; j++) {
        i = i | T(bytes[j]).toUIntMax() << UIntMax(j * 8)
    }
    (...)
}

and one for signed:

func integerWithBytes(bytes:[UInt8]) -> T? {
    (...)
    var i:IntMax = 0
    for (var j = 0; j < maxBytes; j++) {
        i = i | T(bytes[j]).toIntMax() << (j * 8).toIntMax()
    }
    (...)
}

I'm good at that point.

Depends on what type is in context, appropriate function is called. I decided to use IntMax and UIntMax here as the largest integers and perform shift operations on these types.

then another issue pop out:

return i // error

error: IntMax is not convertible to 'T' - This one is because starting with Swift 1.2 I have to explicitly cast types with keyword as.

return i as? T // ok

better, but still...
for some types result is NOT right:

let result:Int32? = integerWithBytes(bytes) // nil

while expected value for signed integer is "-1". I think I should use bitPattern: initializer to solve this issue.

This is going to be madness!

Protocols madness

I've came up with two new protocols to handle another de facto protocol for applying bits to value with bitPattern: initializer:

protocol GenericSignedIntegerBitPattern {
    init(bitPattern: UIntMax)
    init(truncatingBitPattern: IntMax)
}

protocol GenericUnsignedIntegerBitPattern {
    init(truncatingBitPattern: UIntMax)
}

then I have to install new protocols on all integers. For some types I have to add support for init(bitPattern: UIntMax). Notice that Int64 and UInt64 are a bit exceptions to the rules.

extension Int:GenericIntegerType, GenericSignedIntegerBitPattern  {
    init(bitPattern: UIntMax) {
        self.init(bitPattern: UInt(truncatingBitPattern: bitPattern))
    }
}
extension UInt:GenericIntegerType, GenericUnsignedIntegerBitPattern {}
extension Int8:GenericIntegerType, GenericSignedIntegerBitPattern {
    init(bitPattern: UIntMax) {
        self.init(bitPattern: UInt8(truncatingBitPattern: bitPattern))
    }
}
extension UInt8:GenericIntegerType, GenericUnsignedIntegerBitPattern {}
extension Int16:GenericIntegerType, GenericSignedIntegerBitPattern {
    init(bitPattern: UIntMax) {
        self.init(bitPattern: UInt16(truncatingBitPattern: bitPattern))
    }
}
extension UInt16:GenericIntegerType, GenericUnsignedIntegerBitPattern {}
extension Int32:GenericIntegerType, GenericSignedIntegerBitPattern {
    init(bitPattern: UIntMax) {
        self.init(bitPattern: UInt32(truncatingBitPattern: bitPattern))
    }
}
extension UInt32:GenericIntegerType, GenericUnsignedIntegerBitPattern {}
extension Int64:GenericIntegerType, GenericSignedIntegerBitPattern {
    // init(bitPattern: UInt64) already defined
    init(truncatingBitPattern: IntMax) {
        self.init(truncatingBitPattern)
    }
}
extension UInt64:GenericIntegerType, GenericUnsignedIntegerBitPattern {
    // init(bitPattern: Int64) already defined
    init(truncatingBitPattern: UIntMax) {
        self.init(truncatingBitPattern)
    }
}

... now I have three new protocols:

  • GenericIntegerType
  • GenericUnsignedIntegerBitPattern
  • GenericSignedIntegerBitPattern

and finally I can build my generic... ekhm... generics, one for unsigned types:

func integerWithBytes(bytes:[UInt8]) -> T? {
    if (bytes.count < sizeof(T)) {
        return nil
    }

    let maxBytes = sizeof(T)
    var i:UIntMax = 0
    for (var j = 0; j < maxBytes; j++) {
        i = i | T(bytes[j]).toUIntMax() << UIntMax(j * 8)
    }
    return T(truncatingBitPattern: i)
}

and one for signed types. Signed version is slightly different. Here I need do some bitPatten casts, that are not necessary with unsigned types:

func integerWithBytes(bytes:[UInt8]) -> T? {
    if (bytes.count < sizeof(T)) {
        return nil
    }

    let maxBytes = sizeof(T)
    var i:IntMax = 0
    for (var j = 0; j < maxBytes; j++) {
        i = i | T(bitPattern: UIntMax(bytes[j].toUIntMax())).toIntMax() << (j * 8).toIntMax()
    }
    return T(truncatingBitPattern: i)
}

and finally some tests:

let bytes:[UInt8] = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]
integerWithBytes(bytes) as Int8?    // -1
integerWithBytes(bytes) as UInt8?    // 255
integerWithBytes(bytes) as Int16?    // -1
integerWithBytes(bytes) as UInt16?    // 65535
integerWithBytes(bytes) as Int32?    // -1
integerWithBytes(bytes) as UInt32?    // 4294967295 
integerWithBytes(bytes) as Int64?    // -1
integerWithBytes(bytes) as UInt64?    // 18446744073709551615

Code can be found here: gist.github.com/krzyzanowsk…

Conclusion

I'm actually surprised that intigers are so fragmented. Wouldn't be great if have one common place/protocol for all integers, to rule them all? Madness. Finally I came up with result I'm not especially proud of. A lot of time wasted for mediocre result, and I just hope I'm missing something here, something that can change this task to easy one.

If you know better way to solve this puzzle, don't hasetitate contact me, or comment on gist.

PS. Cover image 149a Spicy Mystery Stories Apr-1936 Includes Pit of Madness by E. Hoffmann Price