Commutative operator function synthesis

Hello swift-evolution,

I've on occasion wanted to write operator functions that take two differently typed arguments which could be accepted in any order, for example:

func *(lhs: Vector, rhs: Double) -> Vector { ... }
func *(lhs: Double, rhs: Vector) -> Vector { ... }

Doing the above is a little tiresome when you have a few different operators, though you can reduce it with generics (which may come with some other costs). So a solution I thought would be nice would be to use an attribute that would generate the other corresponding function for you, like so:

@commutative 
func *(lhs: Vector, rhs: Double) -> Vector { ... }

/* Generated code
func *(lhs: Double, rhs: Vector) -> Vector {
   return rhs * lhs
}
*/

So I implemented this because it seemed easy and something I would've liked before. But mainly this was to get my feet wet with the compiler.

But seeing how there is an implementation, I thought I'll post this to see if anyone would actually like this and if it's not too much complexity added for not much gain? FWIW though there isn't much complexity added since it mainly adds onto the pre-existing architecture.

13 Likes

Might this be cleaner as part of the operator declaration? That way you couldn't apply it to arbitrary functions. Otherwise yes, I've wanted the same thing.

Infix + is not commutative for string concatenation.

As to the general idea, there have been previous discussions on these forums. See, for example, this thread.

1 Like

Perhaps we don't need to synthesize a new function that does this, but just reorder the arguments during type checking instead if an operator is @commutative.

1 Like

Commutative is definitely the most common such rule, but I'm a little wary of building up an axiom system one attribute at a time. I.e., I don't think we want to end up with:

@symmetric @reflexive @transitive @impliesSubstitutability
static func ==(_ a: Self, _ b: Self) -> Bool

It's probably worth holding off on adding such a feature until we have a more general need and syntax for it. On the other hand, commutative is the 80% case, so maybe it stands on its own. Personally, I haven't found writing both versions out to be onerous.

6 Likes

I have scrapped many a plan for lack of this functionality. I've had plenty of situations where related but not hierarchically related types can all end up embedded in a common type via operations. Implicit conversions would be another way to handle this, I think

1 Like

Yes, that sounds like an all-around better solution.

What functionality do you see these attributes having? The only thing I can think of these doing is enforcing semantics. But this seems really implausible to me since we'd have to build some sort of model checker to verify them (which would be cool though).

Maybe this is generally a good idea, but your example doesn't look convincing to me: multiplying a vector by a scalar (i.e. double) makes sense, but multiplying a scalar by a vector looks a bit odd to me.

I checked your implementation. That was a big effort and ... quite a big code.

If both arguments have same type (eg Vector+Vector) @commutative is just unnecessary. For different types I don't see a big deal in swapping the arguments, as suggesed by Richard Wei, provided (as far as I can see) there aren't many examples of this kind

These types of axioms also license optimizations. If you know a relation R is symmetric, then a R b is equivalent to b R a, so you can combine two calls into one if both appear. If you know that it's transitive, then the compiler can eliminate a R c if the program has already computed a R b and b R c.

7 Likes

Just throwing an idea out there: what if instead of annotating specific operator functions as commutative, we utilize precedencegroups?

For example, perhaps MultiplicationPrecedence could be defined as

precedencegroup MultiplicationPrecedence {
  ...
  associativity: left
  commutative: true
}

With commutative: true, any operators (such as *) which use the MultiplicationPrecedence group would be treated as commutative.

Going back to the original example...

As @rxwei suggested, when one attempts to perform a Double * Vector operation, the compiler simply reorder the operator function arguments.

What I see as the main advantages of this approach are that

  1. it utilizes a Swift feature that deals with operators already
  2. it helps maintain the consistency of operators; just as we can assume that * will always have a higher precedence than +, with this system, we can always assume that * will be commutative, regardless of the context

This is a non-starter because:

  • * is not commutative for quaternions or matrices, to name just a few.
  • / has MultiplicationPrecedence but is not commutative for almost any type.
  • + is not commutative for strings.
  • - has AdditionPrecedence but is not commutative with almost any type.

(Personally I would be happier if + were always commutative, but that ship sailed a long, long time ago).

5 Likes