Swift currently supports integer literal validation for common programming errors like over- and underflow:
// error: integer literal '600' overflows when stored into 'UInt8'
let a: UInt8 = 600
// ^
It would be useful if custom types that conform to a protocol like ExpressibleByIntegerLiteral
could take advantage of similar compile-time validation. Here are a few example use cases where this functionality could be helpful.
Custom Integer Types
User-defined integer types like UInt24
could benefit from compile-time validation. Currently, custom integers must use runtime checks to verify the literal integer values are within range: https://github.com/apple/swift-nio/blob/master/Sources/NIO/IntegerTypes.swift#L70
Furthermore, user-defined integer types that over- or underflow Int
/ UInt
could take advantage of this protocol to store big numbers.
Type-safe time / date APIs
Types like a theoretical MinuteInHour
that have a clear range (in this case, 0..<60
) could take advantage of compile-time validation for concise, expressive APIs. Take the following example (ignoring all of the numerous time edge-cases, please haha).
func schedule(at hour: HourInDay, _ minute: MinuteInHour) { ... }
schedule(at: 12, 43)
schedule(at: 17, 32)
// error: integer literal '61' is not valid for 'MinuteInHour'
schedule(at: 5, 61)
// ^
Advanced cases
While a simple valid range description for ExpressibleByIntegerLiteral
could solve most of the use cases, I think there are some advanced use cases that are more interesting. For example OddInteger
/ EvenInteger
or even FibonacciNumber
.
Logic beyond simply checking min/max would be required for these types, but could be very interesting:
let a: OddNumber = 1
// error: integer literal '2' is not valid for 'OddNumber'
let b: OddNumber = 2
let c: OddNumber = 3
let a: FibonacciNumber = 1
let b: FibonacciNumber = 2
let c: FibonacciNumber = 3
// error: integer literal '4' is not valid for 'FibonacciNumber'
let d: FibonacciNumber = 4
Rough Sketch
Here's a very rough sketch assuming a new protocol ExpressibleByRawIntegerLiteral
. I'm not really sure how Swift stores an integer-literal that has yet to be converted, but I imagine it could be done as something like a collection of digits alongside some metadata, like isNegative
public struct RawIntegerLiteral {
public enum Digit: Int {
case one = 1, two, ..., eight, nine
}
// true if the integer-literal has a `-` at the beginning
public var isNegative: Bool
public var digits: [Digit]
}
Assuming we have RawIntegerLiteral
, here's what ExpressibleByRawIntegerLiteral
for a type like MinuteInHour
could look like:
public struct MinuteInHour {
let number: Int
// not type-safe, does runtime checks
public init(_ number: Int) {
assert(number >= 0, "Minute cannot preceed 0")
assert(number < 60, "Minute cannot exceed 60")
self.number = number
}
}
extension MinuteInHour: ExpressibleByRawIntegerLiteral {
// type safe, does compile-time checks
public init(rawIntegerLiteral value: RawIntegerLiteral) throws {
guard !value.isNegative else {
throw "minute cannot be negative"
}
switch value.digits.count {
case 1:
// the raw integer only has one digit in the ones' place
self.number = value.digits.[0].rawValue
case 2:
// the raw integer has two digits, check the tens' place
// to ensure it is less than six, this will verify 0..<60 range
guard value[0].digits < .six else {
throw "minute cannot exceed 60"
}
// add the tens' and ones' places to get the actual number
self.number = (value.digits[0].rawValue * 10) + value.digits[1].rawValue
default:
// there is a number in the hundreds' place, definitely too big
throw "minute cannot exceed 60"
}
}
}
// for the sake of concision
extension String: Error { }
Usage would look something like:
let a: MinuteInHour = 0
let b: MinuteInHour = 23
let c: MinuteInHour = 61 // error: minute cannot exceed 60
let d: MinuteInHour = -5 // error: minute cannot be negative
let e: MinuteInHour = 123 // error: minute cannot exceed 60
I'm really interested to know if this is something that has been considered before or whether the necessary machinery exists in the compiler to make it work.
Looking forward to seeing your thoughts, thanks!