22 Logical Operators in Conditional Statements
Real conditions are rarely a single comparison. You combine them, passes the exam and has 75% attendance, region is North or South, not failed. R provides the operators &, |, !, &&, ||, xor(), isTRUE(), and isFALSE() to build these compound conditions, plus %in% for set membership and short-circuit evaluation rules that affect both speed and safety. This chapter explains each operator, the crucial difference between the single and double forms, and how NA propagates through logical expressions.
22.1 The three building blocks
R has three classical logical operators:
| Operator | Name | Meaning |
|---|---|---|
&, && |
AND | TRUE only if both sides are TRUE |
\|, \|\| |
OR | TRUE if either side is TRUE |
! |
NOT | flips TRUE ↔︎ FALSE |
Everything else is built from these three.
22.2 Element-wise vs scalar, the & vs && rule
This is the single most important distinction in R logic.
&and|are element-wise: they compare two vectors position by position and return a vector the same length.&&and||are scalar: they expect a singleTRUE/FALSEon each side and return a single value.
The scalar forms are designed for if statements, where the condition must collapse to one value:
&& with vectors
In R 4.2 and later, passing a vector to && or || raises an error. Older code may “work” by silently using only the first element. Inside if (…), prefer &&/||. Outside if (…), i.e. when filtering vectors, use &/|.
# wrong shape for filtering - uses only first element
df[df$pass && df$paid, ]
# right
df[df$pass & df$paid, ]A simple rule: inside if(…), double; everywhere else, single.
22.3 Short-circuit evaluation
&& and || are short-circuit: they stop evaluating as soon as the answer is decided.
FALSE && anything→FALSE(right side never evaluated)TRUE || anything→TRUE(right side never evaluated)
This matters when the right-hand side might error or is expensive to compute. The classic safety pattern:
If you wrote & instead of &&, R would try to evaluate every clause, including x[1] > 5 on a NULL, and you’d get unexpected behaviour. The element-wise & is not short-circuit.
22.4 NOT, the ! operator
! flips a logical vector. It is element-wise and works on any length.
Use ! to negate a condition: !is.na(x), !duplicated(x), !(region %in% c("North", "South")).
Two identities worth memorising, they let you flip a complicated !(…) into something more readable:
!(a & b)is the same as!a | !b!(a | b)is the same as!a & !b
So !(marks >= 50 & attendance >= 0.75) is equivalent to marks < 50 | attendance < 0.75. Choose the form that reads more naturally.
22.5 xor(), isTRUE(), isFALSE()
Three small helpers that come up often:
xor(a, b) is exclusive-or, TRUE when exactly one side is TRUE.
isTRUE(x) is TRUE only when x is exactly TRUE, not NA, not a vector, not a 1-length numeric. Useful when guarding against unknown input:
isFALSE() is the mirror image. Both are safer than raw == comparisons inside if, where any non-scalar or NA can wreck the condition.
22.6 NA in logical expressions
NA means “unknown.” When R combines an unknown with a known value, the answer is sometimes determined and sometimes not.
R follows what philosophers call three-valued logic: TRUE, FALSE, NA. The result is NA whenever the unknown could change the answer.
This bites in if statements:
Two defensive patterns:
22.7 %in%, set membership
%in% answers “is each element of x somewhere in y?” It is the cleanest replacement for chains of == joined by |.
%in% returns a logical vector of length length(x), never NA. To exclude a set, negate it: !(region %in% c("North","South")).
22.8 Operator precedence
Without brackets, R applies a fixed precedence: comparisons first, then !, then & / &&, then | / ||. The full chain is:
arithmetic → comparisons (<, ==, >=, %in%) → ! → &, && → |, ||
So marks >= 50 & marks <= 90 works as written, because >= and <= are evaluated before &. But mixing & and | without brackets is a recipe for confusion:
R reads & before |, so the condition becomes the first interpretation. Always parenthesise mixed &/| expressions, even when you remember the precedence, the next reader may not.
22.9 Worked example, exam eligibility
Decide whether each student in a class is eligible for the final exam. Rule: at least 50 marks and 75% attendance, or an exemption flag, but not if they are on academic hold.
Three things to notice:
&and|(element-wise), we are filtering a vector, not running anif.- The brackets make the precedence explicit, readers shouldn’t have to memorise the rule.
!class$on_holdcleanly excludes anyone on hold, regardless of the other conditions.
22.10 Summary
| Operator | Element-wise | Scalar | NA-safe? |
|---|---|---|---|
& / \| |
yes | , | propagates NA |
&& / \|\| |
, | yes (R ≥ 4.2 errors on vectors) | propagates NA |
! |
yes | yes | propagates NA |
xor() |
yes | yes | propagates NA |
%in% |
yes | yes | returns FALSE for NA |
isTRUE() / isFALSE() |
, | yes | returns FALSE for NA |
Two rules to live by:
- Inside
if(…), use&&and||. Outside, use&and|. - Always parenthesise mixed
&/|expressions.
Logical operators are tiny but unforgiving, a misplaced & for &&, or a single missing pair of brackets, can quietly invert your analysis. Get into the habit of being explicit and your conditional code will read as clearly as your prose.