Why no compound assignment logical operators?

Why doesn't Swift have &&= and ||= operators?
I tried but couldn't find an answer to this question here or elsewhere.

2 Likes

I suspect it's because C doesn't have them, and I suspect C doesn't have them because it'd be weird to short-circuit in an assignment in C. I've run into this multiple times in Swift, though, so I'd support adding them.

1 Like

Also, in C you can use the |= and &= operators if you don't want short-circuiting, while in Swift you can't.

2 Likes

Speaking of short-circuiting, one slightly complicating issue I can think of is the following.

For example, the following two similar snippets are crucially different:

A:

var b = false
b = foo.frobnicate(by: x) || b
b = foo.frobnicate(by: y) || b
b = foo.frobnicate(by: z) || b

B:

var b = false
b = b || foo.frobnicate(by: x)
b = b || foo.frobnicate(by: y)
b = b || foo.frobnicate(by: z)

(A frobnicates foo tree times using b to see if at least one of them returned true, while B doesn't frobnicate foo at all, and is probably a programmer's error. EDIT: Correction, B frobnicates foo at least once, ie until and including the first that returns true, whereas A always frobnicates foo three times, using b to know if at least one of those three mutating calls returned true. TLDR: Both variants are useful, but clearly different.)

But a hypothetical ||=

var b = false
b ||= foo.frobnicate(by: x)
b ||= foo.frobnicate(by: y)
b ||= foo.frobnicate(by: z)

would only be equivalent to one of them, which one?

And which corresponding one would &&= be equivalent to?

Would it be necessary and/or reasonable to have both variants of each operator?
&&= and =&&
||= and =||

1 Like

I think the behavior of x -= y means that x &&= y is "the same as" x = x && y, i.e. only the right-hand side is delayed. While there are probably some use cases for =&&, it's not really possible to implement that in Swift today, since an inout parameter must always be initialized to something before use.

4 Likes

B will frobnicate foo at least once, so it's not necessarily a mistake. :slightly_smiling_face:

1 Like

Oops, you're right of course, thanks! (added correction to post.) But they're still very different :slight_smile:, and A is at least as useful/common/reasonable as B, if not more so. But B is the one that would be equivalent to ||= ...

I'm not sure what you mean, here's a working example implementation of =|| and ||=, ie both of the two possible different semantics

  • A:   x = y || x
  • B:   x = x || y
infix operator ||=: AssignmentPrecedence
infix operator =||: AssignmentPrecedence
func =||(lhs: inout Bool, rhs: Bool) {
  lhs = rhs || lhs   // A
}
func ||=(lhs: inout Bool, rhs: @autoclosure () -> Bool) {
  lhs = lhs || rhs()   // B
}

I realize the symbols are confusing, but I couldn't come up with a better alternative.

1 Like

=|| always evaluates rhs, so there's no point in it taking an autoclosure, nor even for it existing.
||= lazily evaluates rhs, justifying both its use of an autoclosure and its existence.

true (edited post to fix that)

why not (see motivating example)?

The behavior of the short circuiting version is intuitive since the normal || short circuits, but the eager version is not intuitive.

Anyone looking through code like

var b = false
b =|| foo.frobnicate(by: x)
b =|| foo.frobnicate(by: y)
b =|| foo.frobnicate(by: z)

could very easily mistake it for

var b = false
b ||= foo.frobnicate(by: x)
b ||= foo.frobnicate(by: y)
b ||= foo.frobnicate(by: z)
2 Likes

I am not wholly convinced that this is more readable than

var b = false
if !b { b = foo.frobnicate(by: x) }
if !b { b = foo.frobnicate(by: y) }
if !b { b = foo.frobnicate(by: z) }

I'm not entirely unconvinced, either, mind you.

1 Like

The cases I've wanted it have been more interesting in loops:

var everyFieldIsFooable = true
for nextField in self.fields {
  self.process(nextField)
  everyFieldIsFooable &&= nextField.canFoo
}

You can write this with an if, but in most cases I end up writing everyFieldIsFooable = everyFieldIsFooable && nextField.canFoo because that communicates my intent better. I'm evaluating a compound condition, not doing control flow.

2 Likes

I don't really disagree, but you are doing control flow; the short-circuiting operators always are. I personally think that's fine, but it's a reasonable objection to them that I see from time to time, and applies more to new, less familiar variants.

1 Like

I think Jordan's point is that the compound assignment operators are always understood to be shorthand for lhs = lhs <op> rhs except that the LHS is only evaluated once. It would be very weird for this to be an exception to that. So the semantics are known, and whether those semantics are useful in any particular situation is a separate question.

I would support a proposal to add these and any other missing compound assignments for the standard operators (like ??=), assuming none of them would be actively harmful.

11 Likes
fields.forEach(self.process)
let everyFieldIsFooable = fields.allSatisfy(\.canFoo)

I realize that this is just an example and that &&= may have merit besides this example, but my point is that it is often easy (and helps readability) to write something slightly different.

1 Like

Also, if we either had a throwing overload of &&, or allowed functions expecting a throwing closure argument to act as a candidate for a non-thowing call-site, we could use .reduce(true, &&)

My most recent use case was A:

That is: All the frobnicates must be called (they are mutating foo), no matter the result any of them happens to return. And in the actual use case b had a longer and more descriptive name of course, something like atLeastOneReturnedTrue.

So a "standard" ||= wouldn't be usable here, I'd have needed the other variant =|| ...

I'm pretty sure both variants would be equally usable.

So, now I think:

  • Adding only the "standard" variants (||=, &&=) is awkward, since adding them would be begging for the other variant.

  • Adding both variants (||=, &&=, =||, =&&) is also awkward: The spelling, the exception of these being the only compound assignment operators that has two variants etc ...

I wonder if part of what you're exposing is the lack of a non-short-circuiting or for Bool. If that existed, I would expect to write your example as:

var b = [x,y,z].map(foo.frobnicate).reduce(false, or) // obvious strawman syntax

In C-family languages, you can just use | when you want to avoid short-circuiting, but we don't have that in Swift.

1 Like

The following works as expected though (ie according to case A):

let b = [x,y,z].map({ foo.frobnicate($0) }).reduce(false, { $0 || $1 })

(Have to use closure for map too because otherwise we'll get: "ERROR: Partial application of 'mutating' method is not allowed".)

1 Like