Operator Overloading "+" operator more than 1 chaining gives compiler errors

Hi All,

I'm trying the following code where i'm trying to overload "+" operator with lhs and rhs of different types and returning a new type as a return value.

import Foundation

struct e {
  let index:Int
}

extension e {
  init(_ x:Int) {
    index = x
  }
}

struct GeometricNumber {
  let e:e
  let coefficient:Float
}

extension GeometricNumber : CustomDebugStringConvertible {
  var debugDescription: String {
    "\(coefficient)*e(\(e.index))"
  }
}

precedencegroup eProccessOrder {
  associativity:left
  higherThan: AdditionPrecedence
}

infix operator *:eProccessOrder

func * (_ coeff:Float, _ es:e) -> GeometricNumber {
  GeometricNumber(e: es,coefficient: coeff)
}

func * (_ es:e,_ coeff:Float) -> GeometricNumber {
  GeometricNumber(e: es,coefficient: coeff)
}


enum MathOperations {
  case addition,substraction,multiplication,division
}



struct GeometricExpression {
  var expression:[(GeometricNumber, MathOperations)]
}

extension GeometricExpression:CustomDebugStringConvertible {
  var debugDescription: String {
    return self.expression.map { (first:GeometricNumber, second:MathOperations) -> String in
      return "\(first) \(operation(second))"
    }.joined(separator: " ")
  }
}

func operation(_ op:MathOperations) -> String {
  switch op {
    case .addition:
      return "+"
    case .substraction:
      return "-"
    case .multiplication:
      return "*"
    case .division:
      return "/"
  }
}

precedencegroup geometricExpressions {
  associativity:left
}

infix operator +:geometricExpressions

func + (_ lhs:GeometricNumber, _ rhs:GeometricNumber) -> GeometricExpression {
  var exp = [(GeometricNumber, MathOperations)]()
  exp.append((lhs,.addition))
  exp.append((rhs,.addition))
  return GeometricExpression(expression: exp)
}

func + (_ lhs:GeometricExpression, _ rhs:GeometricNumber) -> GeometricExpression {
  var exp = [(GeometricNumber, MathOperations)]()
  exp.append(contentsOf: lhs.expression)
  exp.append((rhs, .addition))
  return GeometricExpression(expression: exp)
}

func + (_ lhs:GeometricNumber, _ rhs:GeometricExpression) -> GeometricExpression {
  var exp = [(GeometricNumber, MathOperations)]()
  exp.append((lhs, .addition))
  exp.append(contentsOf: rhs.expression)
  return GeometricExpression(expression: exp)
}

let g1 = 10*e(1)
let g2 = 20*e(2)
let g3 = 3.2*e(3)

g1 + g2  // This works fine

g1 + g2 + g3 // This gives compiler error.

here is the code where > 1 chaining has issues

g1 + g2  // This works fine

g1 + g2 + g3 // This gives compiler error.

here is the error

expression failed to parse:
error: pga.playground:101:9: error: cannot convert value of type 'GeometricExpression' to expected argument type 'GeometricNumber'
g1 + g2 + g3
        ^

error: pga.playground:101:11: error: cannot convert value of type 'GeometricNumber' to expected argument type 'GeometricExpression'
g1 + g2 + g3
          ^

I'm not sure if there is any constraint for operator overloading to have lhs and rhs of same type? here "+" operator is just an example I have faced similar issues for many other operators too. Is there any way to hint the compiler to pick the next possible one? Any inputs will be helpful.

I did try out some simpler examples.

Surprisingly the below example works

struct TypeA {
  let a:Int
}

struct TypeB {
  let b:Float
}

struct TypeC {
  let a:TypeA
  let b:TypeB
}

func + (_ lhs:TypeA, _ rhs:TypeB) -> TypeC {
  return TypeC(a:lhs,b:rhs)
}

func + (_ lhs:TypeC, _ rhs:TypeC) -> TypeC {
  return TypeC(a:lhs.a,b:rhs.b)
}


let result = TypeA(a:10) + TypeB(b:10)

TypeA(a:10) + TypeB(b:10) + TypeC(a:TypeA(a:10),b:TypeB(b:10))

However I have all the similar overloads on "+", despite of it this doesn't work.

infix operator +:geometricExpressions

func + (_ lhs:GeometricNumber, _ rhs:GeometricNumber) -> GeometricExpression {
  var exp = [(GeometricNumber, MathOperations)]()
  exp.append((lhs,.addition))
  exp.append((rhs,.addition))
  return GeometricExpression(expression: exp)
}

func + (_ lhs:GeometricExpression, _ rhs:GeometricExpression) -> GeometricExpression {
  var exp = [(GeometricNumber, MathOperations)]()
  exp.append(contentsOf: lhs.expression)
  exp.append(contentsOf: rhs.expression)
  return GeometricExpression(expression: exp)
}

func + (_ lhs:GeometricExpression, _ rhs:GeometricNumber) -> GeometricExpression {
  var exp = [(GeometricNumber, MathOperations)]()
  exp.append(contentsOf: lhs.expression)
  exp.append((rhs, .addition))
  return GeometricExpression(expression: exp)
}

func + (_ lhs:GeometricNumber, _ rhs:GeometricExpression) -> GeometricExpression {
  var exp = [(GeometricNumber, MathOperations)]()
  exp.append((lhs, .addition))
  exp.append(contentsOf: rhs.expression)
  return GeometricExpression(expression: exp)
}

Why "surprisingly"? + and other operators is like static functions and can have arguments of different types.

The following test prints expected ABCD for me.

test
import Foundation

struct GeometricNumber {}
struct GeometricExpression {}

infix operator +

func + (_ lhs:GeometricNumber, _ rhs:GeometricNumber) -> GeometricExpression {
    print("A")
    return .init()
}

func + (_ lhs:GeometricExpression, _ rhs:GeometricExpression) -> GeometricExpression {
    print("B")
    return .init()
}

func + (_ lhs:GeometricExpression, _ rhs:GeometricNumber) -> GeometricExpression {
    print("C")
    return .init()
}

func + (_ lhs:GeometricNumber, _ rhs:GeometricExpression) -> GeometricExpression {
    print("D")
    return .init()
}

func test() {
    let n = GeometricNumber()
    let e = GeometricExpression()
    _ = n + n
    _ = e + e
    _ = e + n
    _ = n + e
}

test()

@tera could you please run the first code of mine and give some answer. Even I'm expecting it to work, but it is not. I also confirmed in my second post that the code I pasted also works with "TypeA/B/C" Example. question is not just "n+n" or "e + e" the syntax "n + n + e" should work in your case and my case. But for some reason it is not working in my first example code, which is what I'm more interested in.

@tera

Strange indeed. + is left associative but even parens don't help in this minimal test:

import Foundation

struct Num {
    static func + (a: Num, b: Num) -> Exp {
        .init()
    }
    static func + (a: Exp, b: Num) -> Exp {
        .init()
    }
}

struct Exp {
}

let n = Num()
let e = Exp()
_ = (n + n) + n // Error: Cannot convert value of type 'Exp' to expected argument type 'Num'

This compiles: ((n + n) as Exp) + n

but I don't see what else the internal (n + n) result could be.

1 Like
This works,
import Foundation

protocol GeometricTerm {
    func add(_ v: GeometricTerm) -> GeometricTerm
}

func + (a: GeometricTerm, b: GeometricTerm) -> GeometricTerm {
    a.add(b)
}

struct GeometricNumber: GeometricTerm {
    func add(_ v: GeometricNumber) -> GeometricExpression {
        fatalError("TODO")
    }
    func add(_ v: GeometricExpression) -> GeometricExpression {
        fatalError("TODO")
    }
    func add(_ v: GeometricTerm) -> GeometricTerm {
        if let v = v as? GeometricNumber {
            return add(v)
        } else if let v = v as? GeometricExpression {
            return add(v)
        } else {
            fatalError()
        }
    }
}

struct GeometricExpression: GeometricTerm {
    func add(_ v: GeometricNumber) -> GeometricExpression {
        fatalError("TODO")
    }
    func add(_ v: GeometricExpression) -> GeometricExpression {
        fatalError("TODO")
    }
    func add(_ v: GeometricTerm) -> GeometricTerm {
        if let v = v as? GeometricNumber {
            return add(v)
        } else if let v = v as? GeometricExpression {
            return add(v)
        } else {
            fatalError()
        }
    }
}

func test() {
    let n = GeometricNumber()
    let e = GeometricExpression()
    _ = n + n + n
    _ = n + n + e
    _ = n + e + n
    _ = n + e + e
    _ = e + n + n
    _ = e + n + e
    _ = e + e + n
    _ = e + e + e
}

but it's not cool.

1 Like

@tera thanks for this solution. I already have something similar in my code. But as you mentioned, it becomes more ugly on the call sight and lot of code to maintain. So I wanted to understand why left associative operators (in my case +) is not chaining along even though required overloads are provided.

BTW, consider using a single enum for your number / expression / etc types - it will be a single type and the issue above will automatically disappear - and no need for a protocol or type casts.

:man_shrugging:

1 Like

Here is some more interesting findings I have.


extension GeometricNumber {
  init(_ e:e) {
    self.e = e
    self.coefficient = 1
  }
}

var f1 = GeometricNumber(e(1))
var f2 = GeometricNumber(e(2))
var f3 = GeometricNumber(e(3))
GeometricNumber(e(1)) + GeometricNumber(e(2)) + GeometricNumber(e(3)) // this works!

f1 + f2 + f3  // This gives compiler error

Important part is here

GeometricNumber(e(1)) + GeometricNumber(e(2)) + GeometricNumber(e(3)) // this works!

f1 + f2 // This works
f1 + f2 + f3  // This gives compiler error

If we directly create the variable and chain along, multiple operator chaining works. Where as if we assign the same to a variable and try to use operator chaining, it works only for the first chaining.

On the contrary with this quoted example following code works even with the assignment to the variable.

let a = TypeA(a:10)
let b = TypeB(b:10)
let c = TypeC(a:TypeA(a:10),b:TypeB(b:10))

a + b + c + c + c  // Compiles and works as expected

Without having looked into the rest of the question, it's important to point out that under no circumstances should you attempt to re-declare standard library operators and assign them to custom precedence groups like this.

This will cause all sorts of unexpected behavior, and I don't think there's any testing on the part of the compiler to make sure that it even works even a little bit sensibly.

1 Like

Hi @xwu thanks for your inputs. Can you please let us know why this is not compiling then? There are no custom precedence group defined here. But still without the additional cast compiler is not able to identify the type.

Shouldn't attempting doing so give a compilation error then?

This is how it can look with enums.
enum Const {
    case i, o
}

enum UnaryOp {
    case minus, sin, cos, sqrt, pow
}

enum BinaryOp {
    case add, sub, mul, div
}

indirect enum Expression {
    case num(Double)
    case const(Const)
    case label(String)
    case unary(_ op: UnaryOp, _ exp: Expression)
    case binary(_ left: Expression, _ op: BinaryOp, _ right: Expression)
    
    static prefix func - (exp: Expression) -> Expression {
        fatalError("TODO")
    }
    
    static func + (left: Expression, right: Expression) -> Expression {
        switch (left, right) {
        case let (.num(a), .num(b)): return Expression.num(a + b)
        case let (.num(a), .const(b)): fatalError("TODO")
        case let (.num(a), .label(b)): fatalError("TODO")
        case let (.num(a), .unary(bop, b)): fatalError("TODO")
        case let (.num(a), .binary(b1, bop, b2)): fatalError("TODO")
        case let (.const(a), .num(b)): fatalError("TODO")
        // ...
        default: fatalError("TODO")
        }
    }
    
    static func + (left: Double, right: Expression) -> Expression {
        fatalError("TODO")
    }
    static func + (left: Expression, right: Double) -> Expression {
        fatalError("TODO")
    }

    static func * (left: Expression, right: Expression) -> Expression {
        fatalError("TODO")
    }
    
    static func * (left: Expression, right: Double) -> Expression {
        fatalError("TODO")
    }

    static func * (left: Double, right: Expression) -> Expression {
        fatalError("TODO")
    }

    static func ^ (left: Expression, right: Expression) -> Expression {
        fatalError("TODO")
    }
    
    static func ^ (left: Expression, right: Double) -> Expression {
        fatalError("TODO")
    }

    static func ^ (left: Double, right: Expression) -> Expression {
        fatalError("TODO")
    }
    
    static func sin(_ exp: Expression) -> Expression {
        .unary(.sin, exp)
    }
    static func cos(_ exp: Expression) -> Expression {
        .unary(.cos, exp)
    }
    static func sqrt(_ exp: Expression) -> Expression {
        .unary(.sqrt, exp)
    }

    static let i = Expression.const(.i)
    static let o = Expression.const(.o)
}

func test() {
    let n: Expression = .o + 123
    let e: Expression = .sqrt(.o^2 + .i^2 + .label("x")^2)
    let one: Expression = -(.e ^ (.i * .pi))

    _ = n + n + n
    _ = n + n + e
    _ = n + e + n
    _ = n + e + e
    _ = e + n + n
    _ = e + n + e
    _ = e + e + n
    _ = e + e + e
}

extension Double {
    static let e = 2.718281828
}

Example expressions:

let e: Expression = .sqrt(.o^2 + .i^2 + .sin(.label("x")))
let one: Expression = -(.e ^ (.i * .pi))

a bit heavy on dots, otherwise normal math.

1 Like

The + operator is heavily overloaded, and one "cheat" to help speed up type checking of expressions was to teach the type checker to favor same-type operands for known operators defined in the standard library.

Unfortunately this can lead to custom uses of the operator not working as they should; fixing this may negatively impact type checking such that many other valid expressions stop compiling, though. (cc @hborla)

If you modify your example to use a custom operator that isn't defined in the standard library (such as +++, for example), you will not have this problem.


Changing the precedence and associativity of standard library operators should absolutely be an error, in my view. That requires an Evolution proposal and I don't have time to do it at the moment.

So, does the following hack work because + subverts being considered an "operator" the first time?

(+)(g1, g2) + g3

Indeed. Minimal example:

struct Num {}
struct Exp {}

infix operator +++ : AdditionPrecedence

func +++ (a: Num, b: Num) -> Exp { .init() }
func +++ (a: Exp, b: Num) -> Exp { .init() }
func +++ (a: Num, b: Exp) -> Exp { .init() }
func +++ (a: Exp, b: Exp) -> Exp { .init() }

func + (a: Num, b: Num) -> Exp { .init() }
func + (a: Exp, b: Num) -> Exp { .init() }
func + (a: Num, b: Exp) -> Exp { .init() }
func + (a: Exp, b: Exp) -> Exp { .init() }

let n = Num()

_ = n +++ (n +++ n) // ok
_ = (n +++ n) +++ n // ok
_ = n +++ n +++ n   // ok

_ = n + n + n       // Error
_ = (n + n) + n     // Error
_ = n + (n + n)     // Error

// Cannot convert value of type 'Exp' to expected argument type 'Num'
// Cannot convert value of type 'Num' to expected argument type 'Exp'

Got the same issue with - * /, no issue with ^

1 Like

@tera This is what @xwu is also mentioning, using a new operator will not have issues.

This approach will again leads to lot of boilerplate code. Instead if you are okay with using "." with operators then you can simply create a new operators for ".+" or ".-" etc as shown below.

infix operator .+ : YourChoiceOfPrecedence

func .+ (_ lhs:TypeA, _ rhs:TypeB) -> TypeC {}


let a = TypeA(a:10)
let b = TypeB(b:10)
let c = TypeC(a:TypeA(a:10),b:TypeB(b:10))

a .+ b .+ c .+ c .+ c  // Compiles and works as expected

This is much less code.
Also I prefer free functions, in that way it opens up the composability of these operators with other functions.

@xwu I'm interested in taking up this task. But have not done such proposals. If you or some one can guide, I would love to take it up.