How do I make a "UInt0"?

[Warning: writing in anger/frustration.]

I'm testing some code. The parameter is generalized on UnsignedInteger. I want to use an empty integer type to activate some corner code. It's harder than I thought. The directions for each protocol (I want Uint0 to conform to UnsignedInteger and possibly FixedWidthInteger.) is 80% straight forward, but I think the cross-over is killing me. Between the half-dozen or so deeper protocols and various requirements, default implementations, and extension-provided methods, I don't know what methods & properties I'm supposed to contribute.

For instance, do I have to provide both BinaryInteger's instance-level bitWidth and FixedWidthInteger's type-level bitWidth, or can one automatically call the other? Then I get frequent complaint to add an init(_ truncatingBits:) initializer, which Apple's web pages don't even list! I'm currently using the latest non-Beta Xcode, which comes with Swift 4.0, but I think the pages are doing 4.1 beta, so I don't know if anything changed. (What would also help is if that SwiftDoc.org site updated to 4.x.)

2 Likes

I realize this may frustrate you further, but I'm having conceptualize the use case for an empty integer type. Could elaborate upon your corner code? I'm genuinely curious to understand why you'd require such a type.

1 Like

You're not going to like the answer, but the answer is that the end user experience of conforming to these protocols is subpar because implementation of the intended final design for integer protocols is unfinished, and it won't be complete before Swift 5. I'm actively working on filling in some of these holes at the moment.

To your specific questions:

  • There is a default implementation of BinaryInteger.bitWidth if you implement static FixedWidthInteger.bitWidth. However! If you work the compiler a little too hard with recursive conformances in implementing Words, apparently this default implementation is "forgotten."

  • init(_truncatingBits:) is undocumented because it shouldn't exist. In Swift 4.0, you have to implement it. The method behaves like init(truncatingIfNeeded:) but is not generic, and is the primitive on which a default init(truncatingIfNeeded:) is implemented. I do believe that, by Swift 4.1, this requirement now has a default implementation so that users will never see it. My goal is to redesign it so that it's gone entirely by Swift 5.0.

A number of other requirements have incorrect documentation in Swift 4.0/4.1, and a small number are implemented incompletely or incorrectly. The design of Words is still being improved, for example, as the compiler allows us to express more useful recursive constraints. The semantics of "masking shift" needs documentation fixes. Primitive dividingFullWidth still doesn't always trap on overflow. Rest assured that it's still being worked on.

My recommendation to you is threefold: (1) build up your type stepwise, first conforming to Numeric, and let the compiler guide you; (2) take a look at the source code for DoubleWidth to see what's necessary; (3) be prepared for new warnings in subsequent versions of Swift as the implementation is finished.

1 Like

The type I'm supposed to being working on has an initializer that takes a word, which is any UnsignedInteger. This means I can use the words property to read the object's value. One part reads the highest-order bits, so I access it with words.last. To do the right thing, I wrap that access if an if-let. So I get an else case, and so I need to test it with a type that generates an empty words.

Semantically, words is guaranteed not to be empty. A proper implementation of UInt0 is expected to have a single element of value 0 as Int. Use words.last! and omit the untestable branch.

Since you are working on these protocols, I have a question for you:
I was recently experimenting with making a natural-number type (i.e. 1...(.max)), and wondered if it was actually semantically correct for such a type to conform to UnsignedInteger and FixedWidthInteger after seeing, in FixedWidthInteger, this:

/// The number of bits used for the underlying binary representation of
/// values of this type.
///
/// An unsigned, fixed-width integer type can represent values from 0 through
/// `(2 ** bitWidth) - 1`, where `**` is exponentiation. A signed,
/// fixed-width integer type can represent values from
/// `-(2 ** (bitWidth - 1))` through `(2 ** (bitWidth - 1)) - 1`. For example,
/// the `Int8` type has a `bitWidth` value of 8 and can store any integer in
/// the range `-128...127`.
public static var bitWidth: Int { get }

/// The maximum representable integer in this type.
///
/// For unsigned integer types, this value is `(2 ** bitWidth) - 1`, where
/// `**` is exponentiation. For signed integer types, this value is
/// `(2 ** (bitWidth - 1)) - 1`.
public static var max: Self { get }

/// The minimum representable integer in this type.
///
/// For unsigned integer types, this value is always `0`. For signed integer
/// types, this value is `-(2 ** (bitWidth - 1))`, where `**` is
/// exponentiation.
public static var min: Self { get }

For these properties, a Nat type can satisfy the general semantics
of these properties, but not the specific values declared.

Thus, would it be correct for a Nat type conform to either of
UnsignedInteger or FixedWidthInteger?

It would not be correct to conform. Many nontrivial implementations rely on these guaranteed semantics; for instance, the floating-point-to-integer conversion functions that were made possible by integer protocols would be impossible to write without them.

Even Numeric conformance would be problematic, I believe, as all of these protocols require the ability to represent 0. For a while, there was a proposed public static var zero, but that was discarded in favor of merely the literal spelling. Still, all types that conform to Numeric must be able to represent the additive identity.

I actually managed to create an Nat∪∞ (InfNat) type which wraps a UInt using 0 as a representation for , and managed to implement all of Numeric, BinaryInteger and FixedWidthInteger except Strideable without calling fatalError() and to the extent that the compiler cannot tell I am doing something wrong (it compiles fine!).

Of course, I just checked, I am using the assumption that an arbitrary BinaryInteger can represent 0, in a type that claims conformance to BinaryInteger that cannot represent 0.
It is horribly, horribly wrong.

Is skipping the multiplicative identity OK? A UInt0 type would have to skip it.

I looked through the Swift source and the Apple docs to start with FixedWidthInteger and see what should be automatically provided. Right now, I got:

    // An unsigned integer of zero bits.
struct UInt0: FixedWidthInteger, UnsignedInteger {
    static func -=(lhs: inout UInt0, rhs: UInt0) {
        let result = lhs.subtractingReportingOverflow(rhs)
        assert(!result.overflow)
        lhs = result.partialValue
    }

    static func -(lhs: UInt0, rhs: UInt0) -> UInt0 {
        var copy = lhs
        copy -= rhs
        return copy
    }

    init<T>(_ source: T) where T : BinaryFloatingPoint { self.init(Int(source)) }

    static func +=(lhs: inout UInt0, rhs: UInt0) {
        let result = lhs.addingReportingOverflow(rhs)
        assert(!result.overflow)
        lhs = result.partialValue
    }

    static func /(lhs: UInt0, rhs: UInt0) -> UInt0 {
        var copy = lhs
        copy /= rhs
        return copy
    }

    static func /=(lhs: inout UInt0, rhs: UInt0) {
        let result = lhs.dividedReportingOverflow(by: rhs)
        assert(!result.overflow)
        lhs = result.partialValue
    }

    static func %(lhs: UInt0, rhs: UInt0) -> UInt0 {
        var copy = lhs
        copy %= rhs
        return copy
    }

    static func %=(lhs: inout UInt0, rhs: UInt0) {
        let result = lhs.remainderReportingOverflow(dividingBy: rhs)
        assert(!result.overflow)
        lhs = result.partialValue
    }

    static func *(lhs: UInt0, rhs: UInt0) -> UInt0 {
        var copy = lhs
        copy *= rhs
        return copy
    }

    static func *=(lhs: inout UInt0, rhs: UInt0) {
        let result = lhs.multipliedReportingOverflow(by: rhs)
        assert(!result.overflow)
        lhs = result.partialValue
    }

    static func +(lhs: UInt0, rhs: UInt0) -> UInt0 {
        var copy = lhs
        copy += rhs
        return copy
    }


    static var bitWidth: Int { return 0 }

    init(_truncatingBits bits: UInt) {}
    init(integerLiteral value: UInt) { self.init(_truncatingBits: value) }

    var byteSwapped: UInt0 { return self }
    var leadingZeroBitCount: Int { return 0 }
    var nonzeroBitCount: Int { return 0 }
    var trailingZeroBitCount: Int { return 0 }

    var words: EmptyCollection<UInt> { return EmptyCollection() }

    func addingReportingOverflow(_ rhs: UInt0) -> (partialValue: UInt0, overflow: Bool) {
        return (self, false)
    }
    func dividedReportingOverflow(by rhs: UInt0) -> (partialValue: UInt0, overflow: Bool) {
        return (self, true)
    }
    func multipliedReportingOverflow(by rhs: UInt0) -> (partialValue: UInt0, overflow: Bool) {
        return (self, false)
    }
    func remainderReportingOverflow(dividingBy rhs: UInt0) -> (partialValue: UInt0, overflow: Bool) {
        return (self, true)
    }
    func subtractingReportingOverflow(_ rhs: UInt0) -> (partialValue: UInt0, overflow: Bool) {
        return (self, false)
    }

    func dividingFullWidth(_ dividend: (high: UInt0, low: Magnitude)) -> (quotient: UInt0, remainder: UInt0) {
        preconditionFailure("Always have division-by-zero")
    }
    func multipliedFullWidth(by other: UInt0) -> (high: UInt0, low: Magnitude) {
        return (self, magnitude)
    }

    static func ==(lhs: UInt0, rhs: UInt0) -> Bool { return true }

    var hashValue: Int { return 0.hashValue }

    static func <(lhs: UInt0, rhs: UInt0) -> Bool { return false }
}

I had to add the init(_truncatingBits:) initializer you mentioned. Technically, all the stuff before bitWidth that the compiler insisted I add shouldn't be necessary; automatic implementations should take of those? I guess this is the compiler-forgets-defaults problem you mentioned.

I'll probably just use words.last!, but I did this quick(?) knock-out out of anger.

Oh, my tests need to go the other way too (Well, just the other way.), beyond UInt64. I guess I could try that DoubleWidth you mentioned.

Nah, those don't have default implementations yet. There are either issues with compilation time or with overload resolution when they're enabled.

Technically the multiplicative identity for UInt0 could be 0. ;-)

2 Likes