Custom Operators: where to put the operator declaration

When defining a custom operator, I am not able to put the declaration for it in the same file as the code for the operator if the code for the operator resides in a different file.

For example, this works (all in the same file):

// main.swift

// Emulating C++ ostream
struct Ostream {
}

// Insertion operators
extension Ostream {
    @discardableResult
    static func endl (_ u: Ostream) -> Ostream {
        print ()
        return u
    }
}

extension Ostream {
    @discardableResult
    static func << (u: Ostream, v: String) -> Ostream {
        print ("\(v)", terminator: "")
        return u
    }
}

extension Ostream {
    @discardableResult
    static func << (u: Ostream, v: Int) -> Ostream {
        print ("\(v)", terminator: "")
        return u
    }
}

extension Ostream {
    @discardableResult
    static func << (u: Ostream, v: Double) -> Ostream {
        print ("\(v)", terminator: "")
        return u
    }
}

extension Ostream {
    @discardableResult
    static func << (u: Ostream, v: (Ostream)->Ostream) -> Ostream {
        return v (u)
    }
}

// ---------------------------------------------------------
//
precedencegroup OstreamPrecedence {
    associativity : left
    lowerThan     : AssignmentPrecedence
}

infix operator << : OstreamPrecedence

// ---------------------------------------------------------
//
let os   = Ostream ()
let endl = Ostream.endl
os << "PI is approximately 22.0/7.0: " << 22.0 / 7.0 << endl

If, however, I put the code for Ostream into a different file:

// Ostream.swift

Expand
// Ostream.swift
//
// Emulating C++ ostream
struct Ostream {
}

// Insertion operators
extension Ostream {
    @discardableResult
    static func endl (_ u: Ostream) -> Ostream {
        print ()
        return u
    }
}

extension Ostream {
    @discardableResult
    static func << (u: Ostream, v: String) -> Ostream {
        print ("\(v)", terminator: "")
        return u
    }
}

extension Ostream {
    @discardableResult
    static func << (u: Ostream, v: Int) -> Ostream {
        print ("\(v)", terminator: "")
        return u
    }
}

extension Ostream {
    @discardableResult
    static func << (u: Ostream, v: Double) -> Ostream {
        print ("\(v)", terminator: "")
        return u
    }
}

extension Ostream {
    @discardableResult
    static func << (u: Ostream, v: (Ostream)->Ostream) -> Ostream {
        return v (u)
    }
}

// ---------------------------------------------------------
//
precedencegroup OstreamPrecedence {
    associativity : left
    lowerThan     : AssignmentPrecedence
}

infix operator << : OstreamPrecedence

// main.swift

// main.swift
//
let os   = Ostream ()
let endl = Ostream.endl
os << "PI is approximately 22.0/7.0: " << 22.0 / 7.0 << endl

I get these errors:

Adjacent operators are in non-associative precedence group 'BitwiseShiftPrecedence'
Binary operator '/' cannot be applied to two 'Ostream' operands
Binary operator '<<' cannot be applied to operands of type 'Double' and '(Ostream) -> Ostream'

But why?

The operator << already exists in Swift, with different precedence than it has in C++. Note that the error mentions “BitwiseShiftPrecedence”. I assume it’s not a bug that you can’t declare a different operator with the same name and expect it to know what you mean.

1 Like

Thank you, but I wanted to know why it works when everything is in the same file, and it doesn't when the declaration resides in a different file.

What's causing the compiler to ignore the custom declaration?

Well, it's a bug. But reassigning an existing standard library operator to a custom precedence group is guaranteed to cause bugs as its behavior is more or less unspecified. You shouldn't do this, whether in the same file or another file. See if you have any problems with defining an actual custom operator.

3 Likes

Thank you, @xwu.

Tried <-, which works but feels quite weird after getting used to << :slight_smile: :

// Ostream.swift
...
precedencegroup OstreamPrecedence {
    associativity : left
    lowerThan     : AssignmentPrecedence
}

infix operator <- : OstreamPrecedence

Yeah, I agree with Xiaodi on both fronts: this clearly should prefer the operator declaration from the current module, but this is also not a good idea to actually do in anything but a toy project.

But, the compiler is able to tell which is which in the following scenario:

precedencegroup OstreamPrecedence {
    associativity : left
    lowerThan     : AssignmentPrecedence
}

infix operator << : OstreamPrecedence

let os   = Ostream ()
let endl = Ostream.endl
os << "PI is approximately 22.0/7.0: " << 22.0 / 7.0 << endl
os << endl
//
os << (1 << 3) << " == " << (2 * 1 << 2) << endl
// PI is approximately 22.0/7.0: 3.142857142857143
// 8 == 8

How would it get confused in general?

I’m not saying it’s a bad idea because the compiler might get confused — that would be a bug. I’m saying it’s a bad idea because other programmers might get confused by an operator not working the way they expect.

7 Likes