Trouble with operator overloading and inferred types in Swift

Viewed 226

Literals

Swift has a couple of literals that can be used to initialize different types without explicitly calling their initializer. For example,

let x = 5             // x is now an Int

automatically infers x to be of type Int. But if we specify the type of the variable to be something else

let x: UInt = 5       // x is now a UInt

the compiler automatically understands that 5 is now an unsigned integer and so the respective UInt initializer has to be called instead of the Int initializer.


The ExpressibleByIntegerLiteral protocol

This is possible for all types that implement the protocol ExpressibleByIntegerLiteral. Now I add this protocol conformance to my own type:

/// Represents a range of characters in a `String`.
//
/// The only purpose of this type is to provide a wrapper around `CountableClosedRange`
/// that can be initialized both with a range and an **integer literal**.
struct TextRange: ExpressibleByIntegerLiteral {

    let range: CountableClosedRange<Int>

    var lowerBound: Int {
        return range.lowerBound
    }

    var upperBound: Int {
        return range.upperBound
    }

    init(_ range: CountableClosedRange<Int>) {
        self.range = range
    }

    // This initializer adds the protocol conformance:
    init(integerLiteral value: Int) {
        self.range = value...value
    }

}

With this definition I can write:

let x: TextRange = 5    // x is now a `TextRange`

However,

let x = 5               // x is now an `Int`

still infers x to be an Int. So far, so good.


Overloading the ... operator

Now I also want to be able to create a TextRange with the closed range operator ... like this:

let y: TextRange = 5...10

so I overload the ... operator in an extension for TextRanges:

extension TextRange {

    /// Given two text ranges, this operator creates a new text range that incorporates
    /// the integers within the operand ranges as well as all integers between the operand ranges.
    ///
    /// - Parameters:
    ///   - lowerRange: A text range whose lower bound is smaller than the lower bound of `upperRange`.
    ///   - upperRange: A text range whose lower bound is greater than the lower bound of `lowerRange`.
    /// - Returns: A `TextRange` from the first operand's lower bound to the second operand's upper bound.
    static func ...(lowerRange: TextRange, upperRange: TextRange) -> TextRange {

        let lowerBound = lowerRange.lowerBound
        let upperBound = max(lowerRange.upperBound, upperRange.upperBound)

        assert(lowerBound <= upperBound, "Cannot create a TextRange because lowerBound > upperBound.")

        return TextRange(lowerBound...upperBound)
    }

}

And this is where I get into trouble. (What a surprise: The trouble begins with operator overloading! )


Trouble

For a variable of TextRange type this operator works as expected but when I omit the type annotation the variable is still instantiated as a TextRange:

let y = 5...10           // y is now a `TextRange`

That shouldn't be because:

  • If no type is specified for a variable,

    the default inferred type of an integer literal is the Swift standard library type Int.

    (see Integer Literals in The Swift Programming Language)

    Thus, I would expect 5 and 10 to be inferred to Int, not TextRange.

  • The overload of the ... operator is only implemented for two operands of TextRange type. Thus, x...y should still use the original Swift implementation of ... if x and y are both Ints.

So why does the compiler "decide" that it should now use the overloaded operator by default for ranges between two integer literals and interpret x and y in the pattern x...y as TextRanges?

Is there a way to tell the compiler which operator should be used by default in case the type of the two operands is not specified?

0 Answers
Related