Why no compound assignment logical operators?

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

Given the relationship between throwing and non-throwing functions, I'm surprised this doesn't already just work. A non-throwing function has the same calling convention as a throwing one, it simply never touches the error-out parameter.

Unless I'm seriously misremembering how that works.

1 Like

The issue isn't the relationship between throwing and non-throwing function types (as you note, the proper subtype relationship exists already), but the fact that the second argument of && is an @autoclosure param, so the full type is (Bool, @autoclosure () throws -> Bool) throws -> Bool. The reduce method expects (Bool, Bool) throws -> Bool, so the types are incompatible.

6 Likes

Yeah, that makes sense. I had a brainfart and completely forgot that this code was trying to pass a Bool to a () -> Bool parameter.

This is close to being a motivating example to add & and | overloads that are (Bool, Bool) -> Bool.

1 Like

Aha! Thanks!

Any reason (except that no one has taken the time) that @autoclosure-argument expecting function couldn’t be used where a normal function is required?

It can automatically wrap an argument expression in a closure When calling directly, but not when partially applying the function.

1 Like

There's been some recent discussion of this in "Adding autoclosure breaks the function's usage?".

2 Likes

I also raised another related issue with the @autoclosure solution for short-circuiting boolean operations here a while back. I'd definitely support a proposal to add non-short-circuiting versions if it doesn't result in serious type-checker issues!