||= and &&= for Bool

It appears there's no ||= nor &&= operators for Bool… is there a reason for that? Is there a near equivalent (that's better than the longhand <expression> = <expression> || <bool>?)

2 Likes

"Because C didn't have those?" Strange indeed. I think we could add them:

infix operator &&=
infix operator ||=

func &&= (lhs: inout Bool, rhs: @autoclosure () -> Bool) {
    lhs = lhs && rhs()
}
func ||= (lhs: inout Bool, rhs: @autoclosure () -> Bool) {
    lhs = lhs || rhs()
}

Edit: removed incorrect equivalent.

3 Likes

Ah, see, your parenthetical goes to a key argument against such an operator:

|| and && short circuit (unlike | and &). Therefore, given x &&= f() and the expected implementation of the compound assignment operators (written out by @tera above), f may or may not be actually called, depending on the value of x. Notably, this would not be the same behavior seen with &= or |=.

This sort of terseness-with-a-footgun is classic for what Swift did not carry over from C/C++ (in the case of &= and |=) or would not even if those languages had the operator (in the case of &&= and ||=).

5 Likes

For what it's worth, JavaScript added &&= and ||= relatively recently, alongside the similarly short-circuiting ??=. I know that Swift doesn't do flow typing the way TS does so a ??= wouldn't make sense in Swift, but that argument doesn't apply to the other two.

1 Like

Yes, as intended and expected given how their longhand forms work.

I mentioned the bitwise forms cavalierly (I'll update my original post), which perhaps misleads here. Swift doesn't support bitwise operations on Bool, in fact. And I'm not advocating changing that. So really we're just talking about the logical operators, ||= and &&=. Therefore there's no possible confusion from having two similar operators that behave differently re. short-circuiting.

That the logical operators short-circuit is universal [in Swift], irrespective of their form (e.g. || vs ||=). I don't see why anyone would be confused by that. Indeed, it'd be terribly confusing if they weren't consistent in that respect.

Consider that the alternatives to a simple ||= are all a lot more boilerplate, e.g. you typically need¹ a custom type for it:

struct Latch {
    var latched = false

    func latchIfNotAlreadyLatched(if expression: @autoclosure () -> Bool) {
        if !latched {
            latched = expression()
        }
    }
}

var latch = Latch()

latch.latchIfNotAlreadyLatched(whatever())

That's a lot of ceremony compared to the three characters of ||=.


¹ Because at least some cases can have neither the LHS evaluated twice nor the RHS evaluated when the latch is already latched. So you can't [always] omit the conditional entirely, and sometimes inlining the conditional - the if expression - is poor ergonomically (and possibly impossible - ViewBuilders maybe, or similar? I know the compiler refuses to let me use if expressions in various 'random' places when working with SwiftUI, for sure).

1 Like

Worth to mention that the following three fragments:

value = value || expression() // aka `||=`, Longhand form
if !value { value = expression() }         // Form-A
if !value && expression() { value = true } // Form-B

while doing seemingly the same are all different in regards to when value's "will/didSet" (if any) is called.

Well, different only in the expected ways, no?

Code
struct A {
    private var silence = false

    var b: Bool {
        willSet {
            if !silence {
                print("\tWill set (current value \(b)).")
            }
        }
        didSet(oldValue) {
            if !silence {
                print("\tDid set from \(oldValue) to \(b).")
            }
        }
    }

    mutating func reset() {
        silence = true
        b = false
        silence = false
    }

    init(b: Bool) {
        self.b = b
    }
}

var a = A(b: false)

print("Normal assignment:")
a.b = true
a.reset()

print("Longhand form (x = x || true):")
a.b = a.b || true
a.reset()

print("Longhand form (x = x || false):")
a.b = a.b || false
a.reset()

print("Conditional guard form A (if !x { x = true }):")
if !a.b { a.b = true }
a.reset()

print("Conditional guard form A (if !x { x = false }):")
if !a.b { a.b = false }
a.reset()

print("Conditional guard form B (if !x && true { x = true }):")
if !a.b && true { a.b = true }
a.reset()

print("Conditional guard form B (if !x && false { x = true }):")
if !a.b && false { a.b = true }
a.reset()
Normal assignment:
	Will set (current value false).
	Did set from false to true.

Longhand form (x = x || true):
	Will set (current value false).
	Did set from false to true.

Longhand form (x = x || false):
	Will set (current value false).
	Did set from false to false.

Conditional guard form A (if !x { x = true }):
	Will set (current value false).
	Did set from false to true.

Conditional guard form A (if !x { x = false }):
	Will set (current value false).
	Did set from false to false.

Conditional guard form B (if !x && true { x = true }):
	Will set (current value false).
	Did set from false to true.

Conditional guard form B (if !x && false { x = true }):

I don't see anything surprising there…?

Whether ||= should trigger a pointless willSet/didSet or not, I'm not sure. Swift tends to do that today, although frankly I'd prefer it didn't. I hate that willSet & didSet are called when the value's not actually changed. I see the conceptual 'correctness' of that, but practically-speaking what you care about 99.9% of the time is willChange or didChange. Alas those aren't included in the built-in hooks.

2 Likes
// will/didSet called? ✔ for yes or ✘ for no
// OR table
value   expression  Longhand  Form-A  Form-B
false     false         ✔       ✔       ✘
false     true          ✔       ✔       ✔
true      false         ✔       ✘       ✘
true      true          ✔       ✘       ✘

Yep… but are you suggesting there's something wrong there, or just summarising?

Ah, no, nothing wrong! Just if one were to go from one form to another (e.g. because they want to introduce "||=" to their code base and thus switch from, say, Form-A or Form-B that they previously had) – there would be this subtle change in behaviour, so I thought it's worth mentioning it.

I would expect ||= and &&= to be equivalent to value = other || expression, short-circuit included and this is in fact how I always implemented them. I'm definitely in favour!

1 Like