Search code examples
swiftinitfailable

Is it possible to limit init values for ExpressibleByIntegerLiteral struct?


I want to implement Digit struct which should be initialized by integer literal.

Like:

let digit: Digit = 5

But swift shouldn't allow doing this (or, as an option, an exception should be raised):

let digit: Digit = 15

I wrote this code but it's not looking good:

   struct Digit: ExpressibleByIntegerLiteral, Equatable, CustomStringConvertible {
    
    typealias IntegerLiteralType = Int
    
    var description: String {
        String(value)
    }
    
    var value: IntegerLiteralType {
        // Unfortunately, set throws can't be implemented, only get throws.
        willSet(newValue) {
            if 0...9 ~= value {
                self.value = newValue
            }
        }
    }


    init(integerLiteral: IntegerLiteralType) {
        if 0...9 ~= integerLiteral {
            value = integerLiteral
        } else {
            // This is definitely not good and may cause side effects.
            // But init for ExpressibleByIntegerLiteral can't be failable.
            value = 0
        }
    }
    
    init?(from char: Character?) {
        guard let char,
              let newValue = IntegerLiteralType(String(char)),
              0...9 ~= newValue
        else {
            return nil
        }
        value = newValue
    }
}

How may I overcome this in Swift 5.8? I guess in next version Macros may be used for this but unfortunately I my Swift version still doesn't have them..

Thank you.


Solution

  • Producing an compiler error for all numbers except 0-9 is not possible, the best you can do is to maximise the range of numbers that would produce a compiler error, by using the smallest IntegerLiteralType, and do fatalError for other numbers.

    struct Digit: ExpressibleByIntegerLiteral {
        let value: UInt8
        
        init(integerLiteral value: UInt8) {
            guard (0...9).contains(value) else {
                fatalError("Digit must be between 0 and 9!")
            }
            self.value = value
        }
    }
    

    Since I used UInt8 as the IntegerLiteralType, things like this would not compile:

    let x: Digit = 1000
    

    And things like this would fatalError:

    let x: Digit = 10
    

    Since there are only 10 valid values for this, an alternative you could consider is using an enum. Enum cases can't start with digits, so you'd need something in front:

    enum Digit: UInt8 {
        case _0
        case _1
        case _2
        case _3
        case _4
        case _5
        case _6
        case _7
        case _8
        case _9
    }