"... and assign" operators alternative

Swift has taken += and similar operators from C. There's quite a few of those "... and assign" operators:

*=		/=		%=		+=		-=
<<=		>>=		&=		|=		^=
&*=		&+=		&-=		&<<=	&>>=
.&=		.|=		.^=

These operators, however ugly they might look initially, served us well so far, both in C (for ~50 years) and in Swift.

However they are not without issues:

  • ugliness (ok, this is subjective and I admit they've grown on me given the amount of time they've been with us, but still there's something wrong in how they look and feel).
  • some operators are unavailable for non-obvious reason: &&= ||=. Perhaps this was inherited from C that didn't have those operators.
  • when you add a custom operator, to make it feel like "built-in" you'd also provide the "... and assign" version of it, there is no automatic generation of those.
  • these operators only work well either for commutative operations or when you are substituting the left hand side: "a = a / b". This doesn't work in many non-commutative cases (e.g. division, an addition to a string: `s = "prefix" + s, matrix multiplication, etc).
  • this approach doesn't support arbitrary functions, only operators (see example below).

Could we do better?

The following pre-pitch idea uses _ as a placeholder for the value being assigned:

foo[0]["a"] = foo[0]["a"] / 2  // full form
foo[0]["a"] /= 2               // existing shortcut
foo[0]["a"] = _ / 2            // proposed shortcut
    
foo[0]["a"] = 2 / foo[0]["a"]  // full form
                               // existing shortcut n/a
foo[0]["a"] = 2 / _            // proposed shortcut

foo[0]["a"] = "!" + foo[0]["a"] // full form
                               // existing shortcut n/a
foo[0]["a"] = "!" + _          // proposed shortcut
 
foo[0]["a"] = max(foo[0]["a"], time) // full form
                               // existing shortcut n/a
foo[0]["a"] = max(_, time)     // proposed shortcut

Edit: this can work for unary operations as well:

foo[0]["a"] = !foo[0]["a"]     // full form
foo[0]["a"].toggle()           // existing shortcut
foo[0]["a"] = !_               // proposed shortcut

foo[0]["a"] = -foo[0]["a"]     // full form
foo[0]["a"].negate()           // existing shortcut
foo[0]["a"] = -_               // proposed shortcut

Alternatively we could use '$' as a placeholder.

In principle having this mechanism could allow us to gradually deprecate the proliferation of "... and assign" operators at some future point and simplify Swift.


Edit: A few further notes.

Let's assume you have a type S and want to add + operation to it:

func + (lhs: Self, rhs: Self) -> Self { // base version
    lhs.adding(rhs)
}

This could serve all cases below:

a = b + c
a = _ + c
a = b + _
a = _ + _

And it would be enough, however you may have a more optimal "mutating func add(Self) -> Void" operation that you want to be used in the "_" cases above. In this case you may want to add one or more variants which you want to optimize:

func + (lhs: inout Self, rhs: Self) { // optionally provided
    lhs.add(rhs)
}
func + (lhs: Self, rhs: inout Self) { // optionally provided
    rhs(lhs)
}

and if compiler sees them defined it picks the most appropriate operator:

a = b + c // `func + (lhs: Self, rhs: Self) -> Self`
a = _ + c // `func + (lhs: inout Self, rhs: Self)`
a = b + _ // `func + (lhs: Self, rhs: inout Self)`
a = _ + _ // `func + (lhs: inout Self, rhs: Self)` or `+ (lhs: Self, rhs: inout Self)`

If you didn't provide, say, the func + (lhs: Self, rhs: inout Self) operation compiler would pick the base version instead:

a = b + _    // `func + (lhs: Self, rhs: Self) -> Self`

Interestingly the same approach could work for arbitrary functions:

func max(_ a: Value, _ b: Value) -> Value // base version
func max(_ a: inout Value, _ b: Value) // optionally provided
func max(_ a: Value, _ b: inout Value) // optionally provided

a = max(b, c) // base version
a = max(_, b) // func max(_ a: inout Value, _ b: Value), if unavailable - then base version
max(&a, b)    // same just spelled out differently
a = max(b, _) // func max(_ a: Value, _ b: inout Value), if unavailable - then base version
max(b, &a)    // same just spelled out differently

Thoughts?

2 Likes

I don’t think this is really needed (+= isn’t ugly to me at all), but the syntax definitely shouldn’t be _. That is used elsewhere to mean “don’t care” or “ignore.” It’s never meant “the thing on the left hand side.” In the future I’d like Swift to explore hole-driven development, and this is precisely the syntax that should have.

But replacing the _ with something else (say $$) doesn’t help IMO. There are many corner cases this raises. How does it work if the LHS is a tuple? If the _ appears multiple times on the right, how is that evaluated? How does this play with didSet? How does it play with property wrappers? I think it introduces an unneeded special case to replace something very uniform.

12 Likes