Let's assume that I have a generic function foo() which returns an unsigned integer whose type is inferred from the context:
func foo<T: UnsignedInteger>() -> T {
return 0 // Simplified example :)
}
let u: UInt = foo()
let u16: UInt16 = foo()
Now I want to define a version for signed integers which calls foo() for the corresponding unsigned integer type, and returns the signed integer with the same bit pattern. In other words,
let i: Int = foo()
should be “equivalent” to
let i = Int(bitPattern: foo())
This is one of my failed attempts:
func foo<T: SignedInteger>() -> T {
return T.init(bitPattern: foo())
// Incorrect argument label in call (have 'bitPattern:', expected 'integerLiteral:')
}
and I wonder if this is possible at all. Am I overlooking something obvious?
It seems that init(bitPattern:) is only defined for all (concrete) integer types, but not part of some protocol, that would be a problem for what I try to achieve.
The closest to that would be Magnitude, which is a type guaranteed to be large enough to store the absolute value of any instance of its Numeric type. It's not guaranteed to be unsigned though, but for all of the built-in integer types it is.
How about:
func foo<T: UnsignedInteger>() -> T {
return 0
}
func foo<T: SignedInteger> -> T where T.Magnitude: UnsignedInteger {
let bitPattern: T.Magnitude = foo()
return T(truncatingIfNeeded: bitPattern)
}
The only place this gets a bit wonky is that it's not guaranteed that a "corresponding signed integer type" exists. It does for all the concrete stdlib types, but it's not guaranteed to exist for an arbitrary type conforming to UnsignedInteger.
What will probably work for your purposes is:
func foo<T: SignedInteger>() -> T where T.Magnitude : UnsignedInteger {
return T(truncatingIfNeeded: foo() as T.Magnitude)
}
This will fall over for a hypothetical non-fixed-width type that it its own magnitude, but will work just fine for all the stdlib types, and more generally for types conforming to FixedWidth & SignedInteger (no fixed-width signed-integer type can possibly be its own magnitude, because the semantics of FixedWidth imply a two's-complement range of values).
Edit: I see that @Nobody1707 gave exactly this solution already. Hopefully my explanation of why it works this way is helpful.
That may be, but I don't yet see how. For a signed integer type T, foo<T>() should call foo<U>() where U is the unsigned counterpart with the same bitwidth as T.
This is where you get into trouble. Conformance to SignedInteger & FixedWidthInteger does not imply that an "unsigned counterpart" exists. It always does for stdlib types, but someone could absolutely implement their own Int73 that doesn't have a corresponding UInt73. This is intentional.
The thing that @Nobody1707 and I sketched will work for all non-pathological cases like this. If you want to handle everything with full generality, you'll need to define your own protocol that provides the constraint you want, and add retroactive conformances for the stdlib types.
// Define a protocol that requires a "corresponding signedness" type exists.
public protocol MartinsInteger : FixedWidthInteger {
associatedtype OtherSignedness : FixedWidthInteger
}
// Retroactively conform some signed types to the new protocol.
extension Int : MartinsInteger { public typealias OtherSignedness = UInt }
extension Int8 : MartinsInteger { public typealias OtherSignedness = UInt8 }
extension Int16 : MartinsInteger { public typealias OtherSignedness = UInt16 }
extension Int32 : MartinsInteger { public typealias OtherSignedness = UInt32 }
extension Int64 : MartinsInteger { public typealias OtherSignedness = UInt64 }
// Now you can define your function for signed types, conforming to the new protocol.
func foo<T>() -> T where T: MartinsInteger, T.OtherSignedness: UnsignedInteger {
return T(truncatingIfNeeded: foo() as T.OtherSignedness)
}
foo() as Int8 // works
For large classes of specific functions foo, however, there are likely to be simpler specific solutions. What are you really trying to do?
@cukr's solution adds a new protocol where conformance means a type can be initialized from a bit pattern represented as an unsigned integer.
My solution adds a new protocol where conformance means that a fixed-width integer type has a "corresponding type with opposite signedness".
Mechanically what they look like in your program is almost identical, but the sets of types that make sense to conform to these two protocols is different (@cukr's suggestion will "work" for arbitrary types that you want to conform, not just integers. That may or may not make sense, depending on the semantics of foo()) and the sent of operations that make semantic sense to do with these operations are different.
Fair question, and sorry for not responding earlier. I was just playing around with some simple (de)serialization code, something like
func read<T: FixedWidthInteger & UnsignedInteger>(from data: Data) -> T {
var value: T = 0
for idx in 0..<MemoryLayout<T>.size {
value = value << 8
value += T(data[idx])
}
return value
}
Here bit shifting on unsigned integers is used to read the value in big-endian order, not relying on the data being aligned properly for type T. Then I tried to define a corresponding
function for signed integers
Did something new happen in the past year and a half to make it this simple?
public extension BinaryInteger {
/// The bits of this integer, in an unsigned variant.
var bitPattern: Magnitude { .init(truncatingIfNeeded: self) }
}
I know there aren't really enough constraints to make that description true, but it's truthy enough for me for now. Is there anything else new which makes the extension unnecessary?
That has always "worked" since Swift 4 (specifically SE-0104). The only real issue is that Magnitude of an arbitrary type conforming to BinaryInteger may not actually be an unsigned type--it could be Self, or some other type (for FixedWidthInteger, it's guaranteed to be unsigned).
The more subtle issue for FixedWidthInteger is that it will be an unsigned integer type, but it may not be the one you want it to be; the OP asked about "the corresponding unsigned integer type", which may not exist, or may not be bound to Magnitude. A hypothetical Int57 might use UInt64 as its Magnitude, and that would be fine.