Why do we need to define separate methods for commutative binary operators?

Eg:

julia> struct abc
       x :: Int
       end

julia> Base.:(+)(a::abc, b::Int) = abc(a.x + b)

julia> abc(1) + 1
abc(2)

julia> 1 + abc(1)
ERROR: MethodError: no method matching +(::Int64, ::abc)

julia> Base.:(==)(a::abc, b::Int) = a.x == b

julia> abc(1) == 1
true

julia> 1 == abc(1)
false

Shouldn’t the second method in each case automatically be defined in terms of the first? This breaks commutativity in case of the equality. A missing method would have been better than this instead of a fallback.

If your method is a subtype of Number and you define appropriate promote rules, you do not have to define both methods.

1 Like

I agree. I was using numbers as an example, and would have expected this to hold for arbitrary types.

You could have a type where + is not commutative so defaulting to it being commutative would be strange.

3 Likes

I think + is expected to be commutative, in fact this is one of the reasons oft repeated for not choosing it as the string concatenation operator.

It’s expected to be, but that is just a norm. That stubborn isn’t coded into the language anywhere.

Alright, but what about equality? Are there types that aren’t commutative under this operation?

1 Like

The thing is that binary operators are just functions like all other functions, they are in no way special-cased in the language. Thus, they obey the same rules as all other functions. You could define == to be non commutative for your own type if you wanted to, because it’s just a regular function.

3 Likes

I wonder if this was considered. There would have to be some way of marking some functions (or some symbols) as being commutative… and perhaps the extra complication was not thought worthwhile?

Mathematica does this:

Attributes[Plus]
{Flat, Listable, NumericFunction, OneIdentity, Orderless, Protected}

Attributes[Dot]
{Flat, OneIdentity, Protected}
3 Likes

It’d be great if there is a way to encode some algebraic properties of binary operators like associativity and commutativity (and approximation of them)! IIUC Fortress uses it to let the compiler automatically parallelize reduction. A relevant RFC PR:

3 Likes

So I actually have an algebra pkg with non-commutative + operation, so + is not always commutative.

No, it would not be worthwhile. Methods should not have properties like this because multiple-dispatch would actually handle this type of issue in Julia already.

2 Likes

Instead of encoding algebraic properties on some functions, it might make sense to make a macro (e.g. @commute) which would define multiple methods in one line. For binary operators it’s doesn’t save very much space (i.e. one line vs two), but if there were methods with three or more inputs which all can be commuted, such a macro would cut the number of method definitions down a lot. Thoughts?

I think this would give you more control over what methods to make commutative or not. For example, you may want different methods for a custom type with a real number like abc(1) + 1 and 1 + abc(1) and other cases where you would want to commute the inputs. In particular, there are probably a lot of functions where a custom type with a missing should return a missing regardless of the order.

@commute Base.:(+)(a::abc, b::Missing) = missing

Encoding algebraic properties into functions could be beneficial if there are some performance optimizations that could occur from some algebraic manipulation. That seems like a lot of complexity to add to highly generalized functions. That being said, defining commuted methods with a macro could be a good first step to implementing such optimizations. Commutativity seems like an obvious property to add as a macro – I’m not as sure of other algebraic properties.

1 Like

On the syntax level, the only multi-argument infix operators are +, *, and the ever-mysterious ++. All other operators just take two arguments and do not chain in infix.

Also, multi-argument forms are only needed for non-associative operators, or if there is some efficiency advantage in doing things in one pass. Otherwise folding works fine by falling back to the 2-argument case.