Integers and integer literals

We have the very handy ExpressibleByIntegerLiteral protocol, which lets custom types be represented by integer literals in source code.

Sometimes, however, we want to convert an existing integer value stored in a variable, to another type which is ExpressibleByIntegerLiteral. This is clearly a sensible thing to do, because there exists an integer literal which represents the integer value.

If that value were a compile-time constant we could just use the literal in source code to initialize the other type. But if the value is computed at runtime—perhaps the count of an array—then it is not obvious how to perform the conversion. There’s no way to turn an integer into an integer literal.

For example, suppose we have a Field protocol that extends Numeric to allow division, and we want to write a function which takes the average of a collection:

extension Collection where Element: Field {
  func average() -> Element {
    return reduce(0, +) / Element(count)
  }
}

This is a compiler error, because there is no initializer matching the call to Element(count). Of course we should be able to convert an integer to a field type—after all there is a canonical homomorphism from the integers to any ring with identity—but none of the protocols involved actually require such an initializer.

I propose we remedy the situation by making it possible to turn an integer into an ExpressibleByIntegerLiteral type. We can do this with a one-line initializer in a protocol extension. Here is a sample implementation:

extension ExpressibleByIntegerLiteral where IntegerLiteralType: BinaryInteger {
  init<T: BinaryInteger>(_ n: T) {
    self.init(integerLiteral: numericCast(n))
  }
}

Wouldn’t that just be implicit conversion?

No, the conversion is explicit. You call an initializer just like any other.

This is a logical error. Simply because all values of type T can be represented in type U it does not follow that all members of type U can be represented in type T.

Consider this type:

struct WhatEvenAreNumbers: ExpressibleByIntegerLiteral {
    private var val: UInt8?

    init(integerLiteral: UInt8) {
        self.val = val
    }

    init() {
        self.val = nil
    }
}

It is clear that we cannot safely convert this to all types that are ExpressibleByIntegerLiteral where IntegerLiteralType == UInt8, or indeed to any type expressible by an integer literal.

Ah, apologies, on re-reading I see you were only proposing to allow BinaryInteger conformances to match. In that case I think the only time it'd be acceptable is if you were allowing it expressly for the case where BinaryInteger == IntegerLiteralType, and in that case you can naturally just call the initializer provided by ExpressibleByIntegerLiteral.

The point is to provide a type conversion from any BinaryInteger to the Expressible type.

We use Int as a currency type, and this is a deliberate design decision, but not every type uses Int as its literal type.

Plus, the init(integerLiteral:) initializer is documented with “Do not call this initializer directly.”

I think that @lantua is right here: this is an implicit type conversion. You initially responded with

and that's true, but misleading. There are two type conversions occurring here the first from the BinaryInteger to the IntegerLiteralType, and then one from the IntegerLiteralType to the ExpressibleByIntegerLiteralType. As I understand it, you are proposing that this:

func t<Integer: BinaryInteger, Expressible: ExpressibleByIntegerLiteral>(_ I: Integer) -> Expressible {
    return Expressible(integerLiteral: Expressible.IntegerLiteralType(I))
}

should be able to be written

func t<Integer: BinaryInteger, Expressible: ExpressibleByIntegerLiteral>(_ I: Integer) -> Expressible {
    return Expressible(integerLiteral: I)
}

Constructed in this way it is clear that you are proposing an implicit type conversion. I think it is right and fair to reject that conversion.

No.

I am proposing that one could write Expressible(n).

This is exactly analogous to writing Float(n).

The FloatingPoint protocol already includes an identical initializer init<T: BinaryInteger>(_ n: T).

I am saying this conversion is more generally applicable than just FloatingPoint. It should work for any Numeric type, and indeed for any ExpressibleByIntegerLiteral type whose IntegerLiteralType is itself a BinaryInteger.

The conversion is explicit, and it is precedented in the standard library.

What you wrote works just about fine:

extension Collection where Element: Field { 
  func average() -> Element { 
    guard let countAsElement = Element(exactly: count) else {
      // You need to handle this case appropriately.
    }
    return reduce(0, +) / countAsElement
  } 
} 
1 Like

I don't think I agree with the generalisation. I'm open to saying it should work for Numeric, and indeed it does work for BinaryInteger, but many types conform to ExpressibleByIntegerLiteral that should not be created from arbitrary binary integers. Indeed, I wrote one.

Things that can be expressed by integer literals in the code are not necessarily integers.

Surely you are not proposing that we eliminate the existing FloatingPoint initializer with identical signature to the one I am proposing, and replace all calls like Double(n) with guard let x = Double(exactly: n) else { … }, right?

I’m sure you recognize the value of the simple call Double(n).

I disagree with both your assertion and your example.

If a type can be constructed from an integer literal, then it can equally well be constructed from the same integer stored in a variable.

No, but also some generic code might be better off if we did. It's important to observe that there are some dangerous edge cases around the naked conversion that deserve attention. What should happen if you average 100,000 Float16 values? 100,000 converts to infinity in Float16, so the average without this check would always be zero or NaN. Is that desirable behavior?

This isn't as bad for floating-point, because the conversion from integer values is fully defined and never traps, it only saturates to out-of-range values; hoisting the init up to Numeric would make it available on a wide range of types that either have never had to define what happens for out-of-range integer conversions, or trap all such conversions, which makes using it in a generic context pretty dangerous.

1 Like

It is expected behavior.

Though I will note that if the value exceeds the maximum of the IntegerLiteralType then the conversion will trap, just as using such a literal would cause a compile-time error.

Swift has been clear and consistent in maintaining that trapping is safe, not dangerous. Swift traps on integer overflow, on out-of-bounds array access, and on force-unwrapping nil.

If a type can be initialized from an integer literal, then it can handle all values of its IntegerLiteralType. A value which overflows that type will of course trap, as expected.