"bitwise and" vs "short-circuiting and", the dot version, which one should be used?

In Julia 1.9, both p .&& q and p .& q are working. But in 1.6.7 LTS, p .&& q is invalid

    p = [true, true, false, false]
    q = [true, false, true, false]

    #p .&& q    #invalid syntax &q
    p .& q

Should I keep to the p .& q version?

But I am thinking the p .&& q is more consistent.

Depends on whether you want the short-circuiting or don’t. Most often you don’t, in usage like this. I prefer to default to &, as it can avoid extra branching.

This seems to be the PR that introduced the change, BTW:

3 Likes

Since this is posted in “New to Julia”: please note that the two operators have completely different precedence for logical comparisons:

julia> x = [1,2,3,4,5];

julia> x .>= 2 .&& x .< 4
5-element BitVector:
 0
 1
 1
 0
 0

julia> x .>= 2 .& x .< 4
5-element BitVector:
 1
 1
 1
 1
 1

julia> (x .>= 2) .& (x .< 4)
5-element BitVector:
 0
 1
 1
 0
 0

Fundamentally, .& is intended for bitwise operations and .&& for logical operations. They both have appropriate precedence for their use case. But be very careful when using .& for logical operations in older versions of Julia - don’t forget to add parentheses!

7 Likes

thanks! the x .>= 2 .& x .< 4 is equivalent to

(x .>= (2 .& x)) .< 4

Hence, for Julia >=1.8, use .&& is better, I think.

I don’t know what convention is the most common, but personally I prefer using & over && for “logical” operations, too; except if I want the short-circuiting behavior. Sometimes the reason for preferring && is that a branch is simply necessary because some code must be executed only conditionally, while in other cases the motivation is performance (avoid executing expensive branch if that’s not necessary).

I would be wary of doing this kind of micro-optimization routinely. Because of the higher precedence mentioned above, & is trickier to use and can lead to less-readable code — I would second @NiclasMattsson in suggesting that you should ordinarily default to using && for all “logical and” operations, and only micro-optimize to & for performance-critical code where benchmarking shows that it helps.

4 Likes

I don’t see the choice of a default AND operator as an optimization at all, though. I mean it’s just a convention, I have to choose one or the other possibility to default to. And I’m not bothered by precedence rules because I like to use extra parentheses anyway when I’m not clear about the precedence.

I guess the most important thing is respecting the style of the surrounding codebase.

Again, since this is “New to Julia” let’s be super clear about the problem with using .& for logical operations.

It’s not just that the bug in x .>= 2 .& x .< 4 is really hard to spot. The major issue is that it fails silently. If you’re lucky you might notice that your code is outputting garbage. In some situations you might never even know your code is broken. Perhaps even worse, you might actually find the bug six months after your paper has been published…

My advice: just default to using .&& for logic unless you really know what you’re doing. Many Julians may never need the bitwise .& operator.

6 Likes

The problem with defaulting to && is that it does not support three-valued logic:

julia> x = [true, false, missing];

julia> y = [true, true, true];

julia> x .&& y
ERROR: TypeError: non-boolean (Missing) used in boolean context

On the other hand, & does support three-valued logic:

julia> x .& y
3-element Vector{Union{Missing, Bool}}:
  true
 false
      missing
5 Likes

This applies to three valued logic imho. missing will error when you want to branch, so you would have to do some fixing than.

Maybe * is more explicit in this case (could be taste).

julia> x = [true, false, missing]; y = [true, true, true];

julia> x .* y
3-element Vector{Union{Missing, Bool}}:
  true
 false
      missing

v1.9.0, not checked on older versions.