R Programming • Syntax and Core Types

Comparison & Logical Operators — Use comparison operators and logical operators (&, &&, |, ||, !)

Master the Boolean logic that drives conditional branching, vectorized filtering, and data subsetting in R.

A statistical scatter plot with code braces and a variable symbol for R programming.
Inside this lesson 10 sections 5 questions 8 figures

SECTION 1

Historical Context & Motivation

The ability to compare values and combine conditions lies at the very foundation of programming, and its roots stretch back well before R itself. Boolean algebra, formalized by George Boole in 1854, provided the mathematical framework for reasoning about truth values—TRUE and FALSE—through conjunction, disjunction, and negation. When John Chambers and his colleagues at Bell Laboratories began designing the S language in the 1970s, they embedded Boolean logic directly into the language's type system, treating logical vectors as first-class citizens. R, created by Ross Ihaka and Robert Gentleman in the early 1990s as an open-source reimplementation of S, inherited this design philosophy and extended it with both element-wise and short-circuit logical operators, a distinction that proves critical when writing robust, efficient code.

1854 • Boole's Laws of Thought

George Boole publishes An Investigation of the Laws of Thought, formalizing logical AND, OR, and NOT as algebraic operations on binary values.

1976 • S Language at Bell Labs

John Chambers designs S with native logical vectors, comparison operators, and vectorized operations—establishing the paradigm R would later adopt.

1993 • R Is Born

Ross Ihaka and Robert Gentleman release R, inheriting S's operator semantics while distinguishing element-wise (&, |) from short-circuit (&&, ||) logical operators.

2000 • CRAN & tidyverse Era

As R's ecosystem matures through CRAN and later the tidyverse, comparison and logical operators become the backbone of dplyr::filter(), data.table subsetting, and vectorized data wrangling at scale.

The central question these operators answer is deceptively simple: How do we express conditions that determine which data to keep, which branches to take, and which computations to perform? In R, the answer involves two distinct families of operators—comparison operators that produce logical values, and logical operators that combine them—each with vectorized and scalar variants that serve different programming contexts.

SECTION 2

Core Principles & Definitions

Before diving into syntax, it is essential to establish the foundational ideas that govern how R evaluates comparisons and logical expressions. R's type system treats logical as one of its six atomic vector types, alongside double, integer, complex, character, and raw. Every comparison and logical operation ultimately produces values of this type: TRUE, FALSE, or NA (logical missing). Understanding the following core principles will make the operator details in subsequent sections intuitive rather than arbitrary.

1

Vectorized by Default

Comparison operators (==, !=, <, >, <=, >=) and element-wise logical operators (&, |, xor()) operate on entire vectors, recycling shorter operands to match longer ones. This is R's most distinctive design choice.
2

Short-Circuit vs. Element-Wise

R provides two AND/OR families: & / | (element-wise, returns a vector) and && / || (short-circuit, returns a single scalar). Choosing the wrong one is a common source of bugs.
3

NA Propagation

Missing values propagate through comparisons and logical operations following three-valued logic. TRUE | NA yields TRUE, but FALSE | NA yields NA. Understanding when NA can be resolved is critical for data analysis.
4

Type Coercion Rules

Logical values coerce to numeric (TRUE → 1, FALSE → 0) and character ("TRUE", "FALSE") as needed. This enables idioms like sum(x > 0) to count elements satisfying a condition, leveraging implicit coercion to integer.
5

Operator Precedence

Negation (!) binds tightest, followed by comparisons, then & / &&, then | / ||. Explicit parentheses are always recommended for clarity.
✦

KEY TAKEAWAY

Think of R's two operator families like two tools for the same job: & and | are like a batch scanner at a warehouse—they inspect every item in a shipment (vector) and stamp each one pass or fail. && and || are like a security guard at a door—they check the first credential, and if that's enough to decide (e.g., a VIP badge), they wave you through without checking anything else. Use the scanner for data vectors; use the guard for control flow.

SECTION 3

Visual Explanation — Operator Taxonomy

The following diagram organizes R's comparison and logical operators into a clear taxonomy, showing the two major families and how they relate. Comparison operators produce logical values from any pair of compatible operands, while logical operators combine those logical values into compound conditions. Notice the critical split between element-wise and short-circuit forms within the logical family.

R Operator Taxonomy: Comparison & LogicalBoolean ExpressionComparison OperatorsLogical OperatorsRELATIONAL (vectorized)== Equal to!= Not equal to< Less than> Greater than<= Less or equal>= Greater or equalELEMENT-WISE(returns vector)& AND| OR! NOTxor() XORSHORT-CIRCUIT(returns scalar)&& AND|| ORPrecedence (highest → lowest):! → < > <= >= == != → & / && → | / ||Use parentheses liberally to make intent explicit
The taxonomy shows how comparison operators feed logical values into logical operators. Element-wise operators (green border) process entire vectors; short-circuit operators (amber border) evaluate only the first element and stop early when the result is determined.

SECTION 4

How It Works — Evaluation Semantics

Comparison Operators

Each comparison operator takes two vectors of compatible types and returns a logical vector of the same length (after recycling). R follows the coercion hierarchy logical < integer < double < complex < character when operands differ in type. For character vectors, comparisons use locale-dependent lexicographic ordering. Crucially, comparing anything to NA always returns NA—even NA == NA is NA, not TRUE. To test for missingness, use is.na().

VECTORIZED COMPARISON

result[i]←x[i]⊕y[iresult[i] ← x[i] ⊕ y[i %% length(y) + 1] for i = 1, …, max(length(x), length(y))

Where ⊕ ∈ {==, !=, <, >, ≤, ≥}. The shorter vector is recycled: index into y uses modular arithmetic. R issues a warning when the longer length is not a multiple of the shorter.

Element-Wise Logical Operators: & , | , !

The element-wise operators follow the same recycling rules as arithmetic. Given two logical vectors p and q, p & q returns a logical vector where position i is TRUE only when both p[i] and q[i] are TRUE. The negation operator ! is unary and flips each element. These operators respect three-valued logic with NA, meaning TRUE & NA yields NA (the result depends on the unknown value), while FALSE & NA yields FALSE (the result is determined regardless of the unknown).

THREE-VALUED AND

p & q → TRUE iff p = TRUE ∧ q = TRUE; FALSE iff p = FALSE ∨ q = FALSE; NA otherwise

The key insight: FALSE dominates AND (FALSE & anything = FALSE), and TRUE dominates OR (TRUE | anything = TRUE). NA results only when the unknown value could change the outcome.

Short-Circuit Logical Operators: && , ||

The short-circuit operators && and || evaluate only the first element of each operand and return a single logical scalar. If the left-hand side determines the result (FALSE for &&, TRUE for ||), the right-hand side is never evaluated—a behavior called short-circuit evaluation. This is essential in if / while statements where you might guard against errors: if (!is.null(x) && x > 0) safely avoids evaluating x > 0 when x is NULL. Starting in R 4.3.0, using && or || with vectors of length greater than one produces a warning, reinforcing that these operators are designed for scalar control flow.

SHORT-CIRCUIT OR

p∣∣q→ifp[1]isTRUE,returnTRUEwithoutevaluatingq;elsereturnq[1]p || q → if p[1] is TRUE, return TRUE without evaluating q; else return q[1]

Only the first elements p[1] and q[1] are used. The right operand q may never be evaluated if p[1] suffices, enabling guard-clause patterns.

SECTION 5

Detailed Breakdown — Truth Tables & NA Behavior

The following truth tables summarize the behavior of R's logical operators under all possible input combinations, including the critical NA cases. Internalizing these tables eliminates guesswork when debugging filter expressions that involve missing data. Notice how the three-valued logic follows a consistent principle: an NA result occurs only when the unknown value could genuinely change the outcome.

pqp & qp | qxor(p, q)!p
TRUETRUETRUETRUEFALSEFALSE
TRUEFALSEFALSETRUETRUEFALSE
FALSETRUEFALSETRUETRUETRUE
FALSEFALSEFALSEFALSEFALSETRUE
TRUENANATRUENAFALSE
FALSENAFALSENANATRUE
NANANANANANA

Complete truth table for R's element-wise logical operators including NA propagation

Short-Circuit vs. Element-Wise Evaluation FlowElement-Wise: x & yShort-Circuit: x && yx = c(T, F, T) y = c(F, T, T)T & FFALSEF & TFALSET & TTRUEResult: c(FALSE, FALSE, TRUE)✓ All 3 pairs evaluated✓ Returns vector of length 3x = c(T, F, T) y = c(F, T, T)x[1] = TRUE?YesMust check y[1]y[1] = FALSEResult: FALSE (scalar)✗ Only x[1] and y[1] examined✗ Returns scalar, not vector✓ If x[1] were FALSE, y skipped
Left panel: the element-wise & operator processes all three element pairs and returns a vector. Right panel: the short-circuit && operator examines only x[1] and y[1], returning a scalar. Had x[1] been FALSE, y[1] would never be evaluated.
⚠

Common Pitfall

Using && or || inside dplyr::filter() or vectorized subsetting is a frequent bug. These contexts require element-wise & and | because you need a logical vector matching the number of rows, not a single scalar. Conversely, if statements require a single logical value, so && and || are the correct choice there.

SECTION 6

Worked Example — Filtering a Data Frame

Suppose you have a data frame of student records and need to extract rows for students who scored above 80 on the midterm and either attended more than 10 lectures or submitted all assignments. Some records have missing midterm scores. This example demonstrates how comparison and logical operators interact with NA values and how to write a robust filter.

Filtering Student Records with Compound Conditions

  1. 1

    Step 1 — Define the Data

    Create a sample data frame with four students. Note that student C has a missing midterm score. students <- data.frame( name = c("Alice", "Bob", "Carol", "Dan"), midterm = c(92, 75, NA, 88), lectures = c(12, 8, 14, 11), all_hw = c(TRUE, FALSE, TRUE, FALSE) )
  2. 2

    Step 2 — Build Individual Conditions

    Apply comparison operators to create logical vectors for each criterion. cond1 <- students$midterm > 80 # c(TRUE, FALSE, NA, TRUE) cond2 <- students$lectures > 10 # c(TRUE, FALSE, TRUE, TRUE) cond3 <- students$all_hw # c(TRUE, FALSE, TRUE, FALSE)
    Carol's midterm condition is NA because NA > 80 returns NA.
  3. 3

    Step 3 — Combine with Element-Wise Operators

    Use & and | (not && / ||) because we need a logical vector matching the number of rows. keep <- cond1 & (cond2 | cond3) # c(TRUE & (TRUE|TRUE), FALSE & (FALSE|FALSE), NA & (TRUE|TRUE), TRUE & (TRUE|FALSE)) # c(TRUE, FALSE, NA, TRUE)
    Carol's result is NA because NA & TRUE = NA—we cannot determine if she passes the midterm threshold.
  4. 4

    Step 4 — Handle NA Before Subsetting

    Subsetting with NA indices includes NA rows, which is usually unwanted. Replace NAs with FALSE using which() (returns only TRUE positions) or the explicit pattern below. safe_keep <- !is.na(keep) & keep # c(TRUE, FALSE, FALSE, TRUE) result <- students[safe_keep, ]
    Final result contains Alice (row 1) and Dan (row 4). Carol is excluded because her midterm is unknown.
  5. 5

    Step 5 — Equivalent dplyr Approach

    In dplyr::filter(), NA rows are automatically dropped. This is syntactic sugar that mirrors our explicit NA handling. library(dplyr) students %>% filter(midterm > 80, lectures > 10 | all_hw)
    Produces the same two-row result. Note: commas inside filter() act as element-wise AND (&), not short-circuit AND.

SECTION 7

Pitfalls, Best Practices & Comparisons

Pitfall / ScenarioWrong PatternCorrect Pattern
Filtering data frame rows with compound conditionsdf[df$x > 0 && df$y < 5, ]df[df$x > 0 & df$y < 5, ]
Guard clause in if statementif (length(x) > 0 & x[1] > 0)if (length(x) > 0 && x[1] > 0)
Testing equality with NAx == NAis.na(x)
Floating-point comparison0.1 + 0.2 == 0.3abs((0.1+0.2) - 0.3) < 1e-10 or all.equal()
Negating a compound condition!x > 5 # negates x, not the comparison!(x > 5)
Comparing vectors of different lengthsRelying on silent recycling without checksEnsure lengths are compatible; use stopifnot(length(x) == length(y))

Common pitfalls with comparison and logical operators in R

💡

RULE OF THUMB

If your expression appears inside if(), while(), or any context expecting a single TRUE/FALSE decision, use && and ||. If your expression appears inside [, subset(), filter(), ifelse(), or any context processing a vector of rows, use & and |. This single heuristic prevents the most common class of logical-operator bugs in R.

SECTION 8

Connection to Advanced Topics

Comparison and logical operators are foundational primitives that underpin many advanced R programming patterns. Understanding their behavior deeply prepares you for topics in functional programming, non-standard evaluation, and high-performance computing within the R ecosystem.

Concept Learned HereAdvanced ExtensionConnection
Element-wise & / |Non-standard evaluation (NSE) in tidyversedplyr captures logical expressions and translates them into SQL WHERE clauses (dbplyr) or optimized C++ (data.table), preserving element-wise semantics.
Short-circuit && / ||Defensive programming & error handlingGuard clauses with tryCatch and short-circuit evaluation prevent errors from propagating, e.g., checking class before method dispatch.
NA propagation in logicSQL three-valued logic & database backendsR's NA semantics mirror SQL's NULL behavior. Understanding three-valued logic transfers directly to writing correct database queries from R.
Vectorized comparisonsS4 / R5 operator overloadingCustom classes can define methods for ==, <, &, etc. via S4 generics, enabling domain-specific comparison semantics (e.g., interval arithmetic).
which() on logical vectorsSparse logical indexing in Matrix / biglmFor large datasets, converting logical vectors to integer indices via which() can reduce memory footprint and speed up subsetting.

How foundational operators connect to advanced R programming

As you progress to topics like building R packages, writing Rcpp extensions, or designing domain-specific languages with rlang, you will find that comparison and logical operators remain the atomic building blocks upon which more sophisticated abstractions are constructed. The clarity you develop now about element-wise versus short-circuit semantics, NA propagation, and operator precedence will save hours of debugging in advanced contexts.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain why TRUE | NA evaluates to TRUE while FALSE | NA evaluates to NA in R. What principle of three-valued logic explains this asymmetry?
Show answer
In three-valued logic, OR returns TRUE whenever at least one operand is TRUE, regardless of the other operand's value. Since TRUE | NA has a TRUE operand, the result is TRUE no matter what NA represents. However, FALSE | NA depends entirely on the unknown value: if NA were TRUE, the result would be TRUE; if NA were FALSE, the result would be FALSE. Because the outcome is indeterminate, R returns NA. The governing principle is that NA propagates only when the unknown value could genuinely change the result.

Question 2: BASIC CALCULATION

Given x <- c(3, 7, NA, 12, 5), what does x >= 5 & !is.na(x) return? Show the intermediate logical vectors for each sub-expression.
Show answer
First, x >= 5 produces c(FALSE, TRUE, NA, TRUE, TRUE). Second, is.na(x) produces c(FALSE, FALSE, TRUE, FALSE, FALSE), so !is.na(x) is c(TRUE, TRUE, FALSE, TRUE, TRUE). The element-wise AND of these two vectors yields c(FALSE, TRUE, FALSE, TRUE, TRUE). Note that position 3 is FALSE (not NA) because FALSE & anything is FALSE—the !is.na(x) check returns FALSE for the NA element, and FALSE dominates AND.

Question 3: INTERMEDIATE

You have a data frame df with columns age (numeric), gpa (numeric, may contain NAs), and enrolled (logical). Write a single base-R expression using bracket subsetting to select all rows where the student is enrolled, over 20 years old, and has a GPA above 3.0—safely handling NA values in GPA. Do not use dplyr or subset().
Show answer
The expression is: df[which(df$enrolled & df$age > 20 & df$gpa > 3.0), ]. Here, which() serves as our NA-safe mechanism: it returns only the indices where the condition is TRUE, silently dropping NA positions. We use & (element-wise) rather than && because we need a logical vector matching the number of rows. An alternative without which() would be mask <- df$enrolled & df$age > 20 & df$gpa > 3.0; df[!is.na(mask) & mask, ].

Question 4: APPLIED

A sensor logs temperature readings every minute into a vector temps of length 1440 (one day). Some readings are NA due to sensor failures. Write R code to compute: (a) how many valid readings fall between 20°C and 30°C inclusive, and (b) the proportion of valid readings that fall outside this range. Use only comparison operators, logical operators, and sum().
Show answer
(a) in_range <- temps >= 20 & temps <= 30; count_in <- sum(in_range, na.rm = TRUE). The element-wise & produces a logical vector where NA entries propagate as NA, and sum(..., na.rm = TRUE) counts TRUE values while ignoring NAs (TRUE coerces to 1). (b) valid <- sum(!is.na(temps)); count_out <- valid - count_in; prop_out <- count_out / valid. Alternatively, prop_out <- sum(!in_range, na.rm = TRUE) / sum(!is.na(temps)). The negation !in_range flips TRUE to FALSE and vice versa, but NA stays NA, so na.rm is still needed.

Question 5: CRITICAL THINKING

Consider the following R expression used inside an if statement: if (is.numeric(x) && length(x) == 1 && x > 0 && log(x) < 10). (a) Why is && the correct choice here instead of &? (b) Explain what would happen if x <- "hello" and we used & instead. (c) Does the order of conditions matter? What if we moved log(x) < 10 to the first position?
Show answer
(a) The && operator is correct because if requires a single logical value, and && short-circuits: it evaluates left to right and stops as soon as any condition is FALSE. This creates a defensive chain where each condition guards the next. (b) With &, all four sub-expressions would be evaluated before combining. When x <- "hello", x > 0 would perform a character comparison (not an error, but semantically wrong), and log(x) would throw an error because log is undefined for character input. With &&, evaluation stops at is.numeric(x) returning FALSE, and log(x) is never called. (c) Order matters critically. If log(x) < 10 were first, passing a non-numeric or negative x would cause a runtime error before the type/length guards could protect execution. Short-circuit conditions should be ordered from cheapest/safest to most expensive/dangerous.

SUMMARY

Summary

Summary

R provides six comparison operators (==, !=, <, >, <=, >=) that produce logical vectors via element-wise evaluation with recycling. These feed into two families of logical operators: element-wise (&, |, !) for vectorized data operations like subsetting and filtering, and short-circuit (&&, ||) for scalar control flow in if and while statements.

Three-valued logic governs NA propagation: an NA result occurs only when the unknown value could change the outcome (FALSE & NA → FALSE because FALSE dominates AND, but TRUE & NA → NA). Always protect subsetting with which() or !is.na() when NAs are possible. Remember: operator precedence runs ! → comparisons → & → |, so use parentheses liberally to make compound conditions unambiguous.

Varsity Tutors • R Programming • Comparison & Logical Operators