Operators and Precedence

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Checked against R 4.6.1 (2026-06-24) on 2026-10-20; R 4.6.2 is scheduled for 2026-10-29. Everything on this page is base R, so no add-on package version applies; the scripts also use the book class of the running shelf scenario, which is plain base R code, and the lintr 3.4.0 default linters were used only to check the scripts.

Operators are where most R programs spend their lines, and most of the surprises in R come from three facts about them: an operator is an ordinary function call written in a different order (Syntax and Lexical Structure showed the parse trees), every operator has a fixed place in one precedence table that you have to know because the parser does not ask, and the comparison operators compare the binary values that are stored, not the decimal numbers you typed. This page goes through the operator families one by one (arithmetic, comparison, logic, membership, matrices, sequences, the pipe, the formula), shows how to define your own and how a class can change the built-in ones, gives the whole precedence table and then the traps it causes, and ends with the one question that every language has to answer for floating-point numbers: when are two things equal?

Every example below is a script that was run with Rscript on R 4.6.1 with warnings printed as they happen (options(warn = 1)), and the lines starting with #> are what it printed. The scripts pass the default lintr linters; where an idiom being shown is exactly what a default linter dislikes (NA == NA, the right assignment 10 -> d, a method name such as %+%.book that is not snake case) the line carries a # nolint: comment. Errors and warnings that a script shows on purpose are caught with tryCatch() and printed as their conditionMessage(), so the text is exactly what R says. The examples use the library scenario of the section: books, loans, shelf_today, loan_days and the book class come from the scenario.R file described on the section index, and the scripts that use them start with source("scenario.R").

Operators are function calls

The R Language Definition lists the operators of the language in a table and then says the sentence that explains the rest of this page: except for the syntax, there is no difference between applying an operator and calling a function, so x + y is the call of a function named + with the arguments x and y, which has to be quoted in backticks to be written as a call because + is not a standard function name (The R Language Definition: Operators). Three consequences are used all through the page:

  • An operator can be passed as an argument and called by its quoted name (Reduce and Map do so in User-defined operators), and the operators can be replaced: the arithmetic, comparison and logical operators are generic functions, so a class can change what they do (see Overriding operators with S3).

  • Most operators are vectorised, so adding two vectors of the same length adds them element by element; the manual warns in the same place that multiplying two matrices does not give the matrix product, which is what %*% is for.

  • The set of operators is fixed by the parser, except for the %name% family, where you choose the name (see User-defined operators).

The next table groups the operators that this page covers. The help page ?Syntax and the manual list the same operators; the manual’s table of operators in the Operators section does not yet list %notin%, which is new in R 4.6.0, and its section on special operators names %% %*% %/% %in% %o% %x% %||% as the predefined %name% operators (The R Language Definition: Special operators).

Operator Meaning Section

+ - * / ^

add or unary plus, subtract or unary minus, multiply, divide, exponentiate (** is read as ^)

Arithmetic

%/%, %%

integer (floored) division and remainder

Arithmetic

< > <= >= == !=

ordering and comparison

Comparison

!, &, |, xor()

negation, and elementwise AND, OR and exclusive OR

Logic

&&, ||

AND and OR on single values, evaluated left to right and only as far as needed

Logic

%in%, %notin%

membership of the left values in the right values

Membership

%||%

the left value unless it is NULL, otherwise the right value

The null-coalescing operator

%*%, %o%, %x%

matrix product, outer product, Kronecker product

Matrix operators and the colon

:

the integer sequence from the left value to the right value

Matrix operators and the colon

|>, \(x), ~, ?

the native pipe, the function shorthand, the formula and help operators

Pipe, lambda, formula and help

<- <<- -> ->> =

assignment, covered on Assignment, Names and Values

Precedence and associativity

:: ::: $ @, [ [[

namespace access, component and slot extraction, indexing, covered on Subsetting and Indexing

Precedence and associativity

%anything%

an operator you define yourself

User-defined operators

Arithmetic

The seven arithmetic operators are +, -, *, /, ^, %% and %/%. They work on numeric and complex vectors, and on anything that can be coerced to them: a logical vector is coerced to integer, with FALSE as 0 and TRUE as 1, so TRUE + TRUE is 2. Applied to arrays they keep the dimensions when that makes sense (R documentation: Arithmetic).

source("scenario.R")

# The seven arithmetic operators, element by element
pages <- c(387L, 432L, 214L)
pages + 10L
#> [1] 397 442 224
pages - 10L
#> [1] 377 422 204
pages * 2L
#> [1] 774 864 428
pages / 100L
#> [1] 3.87 4.32 2.14
pages^2
#> [1] 149769 186624  45796
pages %/% 100L
#> [1] 3 4 2
pages %% 100L
#> [1] 87 32 14

# Types: / and ^ always give doubles, the others keep integers
class(pages + 1L)
#> [1] "integer"
class(pages / 1L)
#> [1] "numeric"
class(pages^2L)
#> [1] "numeric"
class(pages %/% 100L)
#> [1] "integer"
class(pages %% 100L)
#> [1] "integer"
class(TRUE + TRUE)
#> [1] "integer"

The type rule is in the help page: if both arguments are integer, the result of / and ^ is numeric (double) and the result of the other operators is integer; otherwise the result is numeric, or complex if either argument is complex. That is why the first book’s 387 pages give 3.87 for pages / 100L and 3L for pages %/% 100L. The L suffix matters: pages %/% 100 would give a double. See Numbers and Arithmetic for the numeric types themselves.

Floored division and remainder

%/% is floored integer division, q <- x %/% y is floor(x / y), and %% is the remainder r <- x %% y, which therefore takes the sign of y. The help page guarantees, up to rounding error, that x == (x %% y) + y * (x %/% y), unless y is zero. That differs from C, Java and Rust, where % truncates toward zero and takes the sign of the dividend; Python’s % agrees with R.

# Floored division and remainder: the remainder takes the sign of y
-7 %/% 2
#> [1] -4
-7 %% 2
#> [1] 1
7 %/% -2
#> [1] -4
7 %% -2
#> [1] -1
x <- -7
y <- 2
x == (x %% y) + y * (x %/% y)
#> [1] TRUE
5.5 %% 2
#> [1] 1.5
1 %/% 0.2
#> [1] 4
1 %% 0.2
#> [1] 0.2
5 %/% 0
#> [1] Inf
5 %% 0
#> [1] NaN
5L %/% 0L
#> [1] NA
5L %% 0L
#> [1] NA

Four rules in that output are worth remembering:

  • -7 %/% 2 is -4, not -3: the quotient rounds down, and -7 %% 2 is 1, so (-4) * 2 + 1 is -7.

  • Dividing by zero never stops the script for doubles: 5 %/% 0 is Inf and 5 %% 0 is NaN. For integers the result is NA, because an integer has no Inf and no NaN.

  • %% and %/% also work on non-integer values, but the help page warns that the results are subject to representation error. Mathematically 1 %/% 0.2 should be 5, but the binary fraction nearest 0.2 is slightly larger than 0.2, so most platforms give 4, as the output shows, and 1 %% 0.2 is 0.2, not 0. For a double x much larger than y the remainder can lose accuracy, and R warns when it detects that.

  • %/% 7 and %% 7 split a number of days into whole weeks and remaining days, as the last two lines of the loan example do. Both operators bind tighter than * and / (see Precedence and associativity).

The exponent

^ is exponentiation, and R promises 1 ^ y and y ^ 0 are 1 always, even for NA, and that x ^ y gives the proper limit for infinite arguments. A negative base with a fractional exponent has no real answer, so (-8)^(1/3) is NaN: the help page says this comes from the C99 pow function that R calls, not from R. ** is accepted as a synonym of ^, translated by the parser; the help page notes that this was undocumented for many years and that it is deprecated in S, so write ^. Unlike every other arithmetic operator, ^ groups from right to left (2^3^2 is 2^9) and its precedence is higher than that of unary minus (-2^2 is -4); both are in Precedence and associativity.

# ^ and exponents of special values
2^10
#> [1] 1024
2^-1
#> [1] 0.5
(-8)^(1 / 3)
#> [1] NaN
1^Inf
#> [1] 1
0^0
#> [1] 1
NA^0
#> [1] 1

Recycling

When the operands have different lengths, the shorter is repeated to the length of the longer. The warning appears only when the longer length is not a multiple of the shorter, and any operation with a zero-length operand has a zero-length result. The R Language Definition gives the rule under "Recycling rules" (The R Language Definition: Recycling rules). The loan lengths below are differences of two Date vectors, which are also subject to - and the other arithmetic operators through methods for the Date and difftime classes.

# Recycling: shorter operands are repeated, with a warning if it is partial
c(1, 2, 3, 4) + c(10, 20)
#> [1] 11 22 13 24
tryCatch(c(1, 2, 3) + c(10, 20), warning = \(w) conditionMessage(w))
#> [1] "longer object length is not a multiple of shorter object length"
integer(0) + 1L
#> integer(0)

# Loan lengths in days from the shelf scenario
returned <- loans[!is.na(loans$return_date), ]
head(returned$return_date - returned$loan_date, 4L)
#> Time differences in days
#> [1] 21 10  1 26
head(as.integer(returned$return_date - returned$loan_date) %/% 7L, 4L)
#> [1] 3 1 0 3
head(as.integer(returned$return_date - returned$loan_date) %% 7L, 4L)
#> [1] 0 3 1 5

The recycling warning, longer object length is not a multiple of shorter object length, is one of the most common messages in beginner scripts, because it signals that the two vectors were meant to have the same length and do not. Vectorisation and Iteration treats recycling as a design choice; for operators, remember that the zero-length rule hides a different mistake: NULL + 1 is numeric(0) rather than an error, and a later if on it fails with argument is of length zero.

Arithmetic on the wrong types

A character vector, a list or a data frame is not number-like, so arithmetic on it is an error. A factor is stored as integers, but its arithmetic is blocked on purpose with a warning that returns NA. The messages are short and never name the offending value, so look at the types first.

# Arithmetic on the wrong types
tryCatch("a" + 1, error = \(e) conditionMessage(e))
#> [1] "non-numeric argument to binary operator"
tryCatch(-"a", error = \(e) conditionMessage(e))
#> [1] "invalid argument to unary operator"
tryCatch(list(1) + 1, error = \(e) conditionMessage(e))
#> [1] "non-numeric argument to binary operator"
tryCatch(books + 1, error = \(e) conditionMessage(e))
#> [1] "non-numeric argument to binary operator"
tryCatch(factor("a") + 1, warning = \(w) conditionMessage(w))
#> [1] "‘+’ not meaningful for factors"
NULL + 1
#> numeric(0)
TRUE + TRUE
#> [1] 2

The two messages to recognise are non-numeric argument to binary operator (a character, list or data frame on either side of a binary operator) and invalid argument to unary operator (the same for -x and +x). The quote characters in ‘+’ not meaningful for factors depend on the locale R runs in; in a non-UTF-8 locale they are plain apostrophes.

Integer overflow

Integers are 32-bit, so arithmetic that leaves the range of about plus or minus 2.1 billion returns NA with a warning, whereas the same sum in doubles is exact. A literal such as 1 is a double, and a double holds whole numbers exactly up to 2^53, so the same arithmetic on doubles does not overflow in this way.

# Integer overflow becomes NA with a warning
tryCatch(.Machine$integer.max + 1L, warning = \(w) conditionMessage(w))
#> [1] "NAs produced by integer overflow"
.Machine$integer.max + 1
#> [1] 2147483648

Comparison

The six comparison operators <, >, <=, >=, == and != return a logical vector with one element per pair of operands, recycled like arithmetic. They are generic functions too, and the help page spells out the coercion rule: when the operands have different types one is coerced to the other, in the decreasing order character, complex, numeric, integer, logical and raw (R documentation: Comparison).

source("scenario.R")

# Element-wise comparison gives a logical vector the length of the operands
books$year < 2000L
#>  [1]  TRUE  TRUE FALSE FALSE FALSE FALSE    NA  TRUE FALSE FALSE FALSE FALSE
#> [13] FALSE FALSE FALSE FALSE
sum(books$year < 2000L, na.rm = TRUE)
#> [1] 3
books$title[books$pages >= 400L & !is.na(books$pages)]
#> [1] "Pride and Prejudice" "Notes on Gravity"

# NA and NaN are not comparable, not even with themselves
NA == NA # nolint: equals_na_linter.
#> [1] NA
NA > 1
#> [1] NA
NaN == NaN
#> [1] NA
is.na(NA_integer_)
#> [1] TRUE
books$pages == 214L
#>  [1] FALSE FALSE  TRUE FALSE FALSE    NA FALSE FALSE FALSE FALSE FALSE FALSE
#> [13] FALSE FALSE    NA FALSE
which(books$pages == 214L)
#> [1] 3

# Mixed types: the operands are coerced to the "higher" type
1L == 1
#> [1] TRUE
1 == "1"
#> [1] TRUE
TRUE == 1L
#> [1] TRUE
"TRUE" == TRUE
#> [1] TRUE
"10" < "9"
#> [1] TRUE
10 < "9"
#> [1] TRUE

The output shows the rules that cause most comparison bugs:

  • NA and NaN are "non-comparable even to themselves", so every comparison involving them is NA. NA == NA is NA, not TRUE, and x == NA is always a vector of NA; use is.na(). A comparison against a column with missing values keeps those positions NA, as books$pages == 214L shows, and which() drops them while books[books$pages == 214L, ] would insert a row of NA.

  • 1 == "1" is TRUE because the number is turned into the string "1". The same coercion explains 10 < "9": both sides become strings, and the string "10" sorts before "9". Convert deliberately (as.numeric()) before comparing values that came from text.

  • Logical values are numbers for comparison, so TRUE == 1L.

Comparing strings

Strings are compared by the collating sequence of the current locale, which is not ASCII order in most locales: in the example "a" < "B" is TRUE and sort() puts a before A. The help page warns that collation differs between locales (in Estonian Z comes between S and T, in Danish aa sorts after z) and between platforms, so do not rely on a particular order across machines. sort(x, method = "radix") always sorts in the C locale, which is the portable order for keys. Encodings, Locales and Internationalization covers Sys.setlocale() and the ICU collator.

# Strings are compared in the collation order of the locale
"a" < "B"
#> [1] TRUE
sort(c("b", "A", "a", "B"))
#> [1] "a" "A" "b" "B"
sort(c("b", "A", "a", "B"), method = "radix")
#> [1] "A" "B" "a" "b"

Recycling, factors and other types

# Recycling, as in arithmetic
c(1, 2, 3, 4) == c(1, 2)
#> [1]  TRUE  TRUE FALSE FALSE
tryCatch(c(1, 2, 3) == c(1, 2), warning = \(w) conditionMessage(w))
#> [1] "longer object length is not a multiple of shorter object length"

# Factors compare by their labels, and only for equality
genre <- factor(c("Fiction", "Poetry", "Fiction"))
genre == "Fiction"
#> [1]  TRUE FALSE  TRUE
tryCatch(genre < "Poetry", warning = \(w) conditionMessage(w))
#> [1] "‘<’ not meaningful for factors"

# Lists and functions
list(1, 2) == c(1, 3)
#> [1]  TRUE FALSE
tryCatch(list(1, 2:3) == 1, error = \(e) conditionMessage(e))
#> [1] "'list' object cannot be coerced to type 'double'"
tryCatch(sum == mean, error = \(e) conditionMessage(e))
#> [1] "comparison (==) is possible only for atomic and list types"
tryCatch(quote(a + b) < 1, error = \(e) conditionMessage(e))
#> [1] "comparison (<) is not possible for language types"
quote(a + b) == "a + b"
#> [1] TRUE
  • Comparing factors with == and != compares the labels, and the ordering operators give NA with a warning unless the factor is ordered (see Factors).

  • A list can be compared with an atomic vector only if every element has length one and can be coerced; otherwise the error is 'list' object cannot be coerced to type 'double'.

  • A function is not comparable: comparison (==) is possible only for atomic and list types. To test two functions for the same definition use identical() (see Exact equality with identical()).

  • A language object (a symbol or a call) can be used with == and != only; R deparses it to a string and compares the strings, so quote(a + b) == "a + b" is TRUE, and < gives comparison (<) is not possible for language types. The help page itself says identical() is usually the better choice for equality of such objects.

The help page ends its Note with advice that the next sections develop: do not use == or != where if needs a single TRUE or FALSE, and for numbers use all.equal() with isTRUE() instead of ==.

Logic: vectorised and short-circuit

The logical operators come in two families (R documentation: Logic). !, & and | work on whole vectors, element by element, like arithmetic, and xor() is the elementwise exclusive OR. && and || work on single values, evaluate left to right and stop as soon as the result is decided; the help page calls the long forms "appropriate for programming control-flow and typically preferred in if clauses".

source("scenario.R")

# Vectorised operators: ! & | xor, element by element
borrowed <- c(TRUE, TRUE, FALSE, FALSE)
overdue <- c(TRUE, FALSE, TRUE, FALSE)
!borrowed
#> [1] FALSE FALSE  TRUE  TRUE
borrowed & overdue
#> [1]  TRUE FALSE FALSE FALSE
borrowed | overdue
#> [1]  TRUE  TRUE  TRUE FALSE
xor(borrowed, overdue)
#> [1] FALSE  TRUE  TRUE FALSE

# Numbers are coerced: zero is FALSE, everything else TRUE
!0:2
#> [1]  TRUE FALSE FALSE
c(0, 2, NA) | c(0, 0, 0)
#> [1] FALSE  TRUE    NA

# NA in logic: the result is NA only when it is ambiguous
NA & TRUE
#> [1] NA
NA & FALSE
#> [1] FALSE
NA | TRUE
#> [1] TRUE
NA | FALSE
#> [1] NA
!NA
#> [1] NA
xor(NA, TRUE)
#> [1] NA

# Raw vectors: bitwise
as.raw(12L) & as.raw(10L)
#> [1] 08
as.raw(12L) | as.raw(10L)
#> [1] 0e
!as.raw(12L)
#> [1] f3

The rules behind that output:

  • Numbers are coerced to logical: zero is false and every other number is true, so !0:2 is TRUE FALSE FALSE.

  • NA is a valid logical value and the result is NA only when it is ambiguous: NA & TRUE is NA, but NA & FALSE is FALSE, and NA | TRUE is TRUE.

  • Raw vectors are handled bitwise by !, &, | and xor(), and ! is the ones' complement. For bit operations on integers use bitwAnd() and its relatives.

  • The AND operators have higher precedence than the OR operators. The help page points out that this differs from S, where & and | (and && and ||) had equal precedence, so TRUE || TRUE && FALSE is TRUE.

Short-circuit evaluation

The right operand of && is evaluated only when the left is TRUE, and the right operand of || only when the left is FALSE or NA. That makes the second operand a safe place for a test that would fail if the first test were false, as !is.null(isbn) && nzchar(isbn) does, and it means a side effect or an error on the right may never happen.

# Short-circuit operators && and ||
TRUE && FALSE
#> [1] FALSE
FALSE && stop("never evaluated")
#> [1] FALSE
TRUE || stop("never evaluated")
#> [1] TRUE
tryCatch(TRUE && stop("evaluated now"), error = \(e) conditionMessage(e))
#> [1] "evaluated now"
NA && FALSE
#> [1] FALSE
NA && TRUE
#> [1] NA
NA || TRUE
#> [1] TRUE

The length-1 errors of && and ||

&& and || take exactly one value on each side. Older versions of R used only the first element of a longer vector, which hid mistakes such as books$year > 2000 && x. The NEWS entries are, in order:

  • R 4.2.0: "Calling && or || with either argument of length greater than one now gives a warning (which it is intended will become an error)" (R NEWS: Changes in R 4.2.0). The same release made if() and while() with a condition of length greater than one an error.

  • R 4.3.0: "Calling && or || with LHS or (if evaluated) RHS of length greater than one is now always an error", with a report of the form 'length = 4' in coercion to 'logical(1)'; the environment variable _R_CHECK_LENGTH_1_LOGIC2_ no longer has any effect (R NEWS: Changes in R 4.3.0).

# Length one only (error since R 4.3.0, warning in R 4.2.0)
tryCatch(c(TRUE, TRUE) && TRUE, error = \(e) conditionMessage(e))
#> [1] "'length = 2' in coercion to 'logical(1)'"
tryCatch(TRUE || c(TRUE, FALSE), error = \(e) conditionMessage(e))
#> [1] TRUE
logical(0) || TRUE
#> [1] TRUE
logical(0) || FALSE
#> [1] NA
logical(0) && TRUE
#> [1] NA
tryCatch(books$year > 2000L && TRUE, error = \(e) conditionMessage(e))
#> [1] "'length = 16' in coercion to 'logical(1)'"
tryCatch(if (books$year > 2000L) 1, error = \(e) conditionMessage(e))
#> [1] "the condition has length > 1"

Notice what is and is not an error. The check applies to an operand that is evaluated, which is why TRUE || c(TRUE, FALSE) still returns TRUE: the right side is never looked at. A zero-length operand is not an error either, it becomes NA, so logical(0) || FALSE and logical(0) && TRUE are NA. books$year > 2000L && TRUE fails with the length of the comparison (here 16), and if (books$year > 2000L) fails with the condition has length > 1. The fix is a reduction that says what you mean: any(books$year > 2000L, na.rm = TRUE), all(...), or a subset with &. The if rule is on Control Flow.

Wrong types

! of a character vector gives invalid argument type. The elementwise & and | need number-like operands and say operations are possible only for numeric, logical or complex types. The short-circuit forms name the operand: invalid 'x' type in 'x && y' (and 'y' for the right side). A NULL operand is a wrong type for &&, not a zero-length logical.

# Wrong types
tryCatch(!"a", error = \(e) conditionMessage(e))
#> [1] "invalid argument type"
tryCatch("a" & TRUE, error = \(e) conditionMessage(e))
#> [1] "operations are possible only for numeric, logical or complex types"
tryCatch("a" && TRUE, error = \(e) conditionMessage(e))
#> [1] "invalid 'x' type in 'x && y'"
tryCatch(NULL && TRUE, error = \(e) conditionMessage(e))
#> [1] "invalid 'x' type in 'x && y'"
tryCatch(list(TRUE) | TRUE, error = \(e) conditionMessage(e))
#> [1] "operations are possible only for numeric, logical or complex types"

any, all, xor, isTRUE and isFALSE

any() and all() reduce a logical vector to one value, and are the cure for the length-1 errors above. all(logical(0)) is TRUE and any(logical(0)) is FALSE, as in logic: "all of nothing" holds vacuously. Both accept na.rm = TRUE; without it, a single NA can make the result NA (see Missing and Special Values).

isTRUE(x) and isFALSE(x) are the safe way to test a value that might be NA, the wrong length or the wrong type. The help page defines isTRUE(x) as { is.logical(x) && length(x) == 1 && !is.na(x) && x }, isFALSE() analogously, and says that if (isTRUE(cond)) may be preferable to if (cond) because of NA values. Older versions defined isTRUE <- function(x) identical(x, TRUE), which was false for a named value such as c(val = TRUE); the current definition is true for it.

# Reducing a vector to one value: any(), all()
any(books$year > 2020L, na.rm = TRUE)
#> [1] TRUE
all(books$year > 1800L, na.rm = TRUE)
#> [1] TRUE
any(books$year > 2020L)
#> [1] TRUE
all(logical(0))
#> [1] TRUE
any(logical(0))
#> [1] FALSE

# isTRUE() and isFALSE(): exactly one TRUE (or FALSE), never NA
isTRUE(TRUE)
#> [1] TRUE
isTRUE(NA)
#> [1] FALSE
isTRUE(c(TRUE, TRUE))
#> [1] FALSE
isTRUE(c(val = TRUE))
#> [1] TRUE
isTRUE(1L)
#> [1] FALSE
isFALSE(FALSE)
#> [1] TRUE
isFALSE(NA)
#> [1] FALSE
x <- NA
if (isTRUE(x)) "yes" else "no"
#> [1] "no"
tryCatch(if (x) "yes" else "no", error = \(e) conditionMessage(e))
#> [1] "missing value where TRUE/FALSE needed"

# A condition made safe with && and isTRUE
has_isbn <- function(isbn) {
  !is.null(isbn) && length(isbn) == 1L && !is.na(isbn) && nzchar(isbn)
}
has_isbn(NULL)
#> [1] FALSE
has_isbn(NA_character_)
#> [1] FALSE
has_isbn("")
#> [1] FALSE
has_isbn("9780306406157")
#> [1] TRUE

if (x) with x equal to NA is the error missing value where TRUE/FALSE needed, and with a zero-length x it is argument is of length zero. isTRUE() turns both cases into an ordinary FALSE, which is right when "not known" should take the else branch and wrong when it should stop the script; choose on purpose, and see Defensive Programming and Input Validation for checks that stop.

Membership: %in% and %notin%

x %in% table is a logical vector with one element per element of x: is it found in table? It is defined in the help page of match() as match(x, table, nomatch = 0L) > 0L, so it never returns NA, which the help page highlights as making it "particularly useful in if conditions". Missing values are matched by the rules of match(): NA matches NA and no other value, and NaN matches NaN but not NA.

source("scenario.R")

# %in% asks "is each element of the left side found in the right side?"
c("Fiction", "Poetry", "Drama") %in% c("Fiction", "Poetry")
#> [1]  TRUE  TRUE FALSE
trimws(tolower(books$genre)) %in% c("fiction", "poetry")
#>  [1] FALSE FALSE  TRUE  TRUE FALSE FALSE  TRUE FALSE FALSE  TRUE  TRUE FALSE
#> [13] FALSE FALSE  TRUE FALSE
sum(trimws(tolower(books$genre)) %in% c("fiction", "poetry"))
#> [1] 6

# It never returns NA, unlike ==
NA %in% c(1, NA)
#> [1] TRUE
NA %in% 1:3
#> [1] FALSE
NA == 1 # nolint: equals_na_linter.
#> [1] NA
c(1, NA) %in% c(NA, 2)
#> [1] FALSE  TRUE
NaN %in% c(NA, NaN)
#> [1] TRUE
NA %in% NaN
#> [1] FALSE

%notin% is the negation, new in R 4.6.0: the NEWS entry says “x %notin% table newly in base is an idiom for !(x %in% table) and provided almost entirely for convenience and code readability” (R NEWS: Changes in R 4.6.0). The help page notes that it could be defined as Negate() applied to %in% but is match(x, table, nomatch = 0L) == 0L for symmetry and efficiency, so it never returns NA either.

# %notin% (R 4.6.0) is the negation
c("Fiction", "Poetry", "Drama") %notin% c("Fiction", "Poetry")
#> [1] FALSE FALSE  TRUE
loans$isbn[1:3] %notin% books$isbn
#> [1] TRUE TRUE TRUE
identical(
  c(1, 5) %notin% 1:3,
  !(c(1, 5) %in% 1:3)
)
#> [1] TRUE

# %in% binds tighter than !, so the parentheses are optional (but kinder)
!1:10 %in% c(2, 3, 5, 7)
#>  [1]  TRUE FALSE FALSE  TRUE FALSE  TRUE FALSE  TRUE  TRUE  TRUE
!(1:10 %in% c(2, 3, 5, 7))
#>  [1]  TRUE FALSE FALSE  TRUE FALSE  TRUE FALSE  TRUE  TRUE  TRUE

The last lines of that chunk show a precedence fact: %in% binds tighter than !, so !1:10 %in% c(2, 3, 5, 7) means !(1:10 %in% c(2, 3, 5, 7)). That is what you want, but the ?Syntax example says "we strongly advise to use the !( ... ) form in this case", because it is easy to misread; %notin% removes the problem on R 4.6.0 and later. Code that has to run on an older R keeps the parentheses or defines its own helper under a different name.

match, is.element and type coercion

match(x, table) returns the position of the first match (or nomatch, NA by default) and is the right tool when the position is needed, for example to look up a value in a key vector. is.element(x, y) is the S-compatible equivalent of %in%. The operands are coerced to a common type (logical, integer, numeric, complex, character in that order), factors, raw vectors and lists are converted to character vectors first, and a Date is matched against characters after converting the characters to dates.

# match() returns the position of the first match, %in% only a logical
match(c("Poetry", "Drama", "Fiction"), c("Fiction", "Poetry"))
#> [1]  2 NA  1
match(c("Poetry", "Drama"), c("Fiction", "Poetry"), nomatch = 0L)
#> [1] 2 0
c("a", "b") %in% list("a", 1)
#> [1]  TRUE FALSE
1 %in% factor(c(1, 2))
#> [1] TRUE
as.Date("2026-06-30") %in% c("2026-06-30", "2026-07-01")
#> [1] TRUE
is.element(3, 1:5)
#> [1] TRUE

# match() coerces to a common type
1 %in% "1"
#> [1] TRUE
TRUE %in% "TRUE"
#> [1] TRUE
1L %in% 1.0
#> [1] TRUE

Two cautions. First, matching a list is potentially very slow, as the help page says, and is best avoided. Second, matching of doubles is exact: 0.3 %in% seq(0, 1, by = 0.1) is FALSE (see Floating-point comparisons).

The null-coalescing operator

x %||% y returns x unless it is NULL, in which case it returns y. It was added in R 4.4.0; the NEWS entry reads “L %||% R newly in base is an expressive idiom for the phrases if(!is.null(L)) L else R or if(is.null(L)) R else L” (R NEWS: Changes in R 4.4.0). It is documented, rather surprisingly, on the help page of the control-flow keywords: "Inspired by Ruby, it was first proposed by Hadley Wickham" (R documentation: Control). The Logic help page lists it under See Also.

# The null-coalescing operator %||% (R 4.4.0): left side unless it is NULL
NULL %||% "default"
#> [1] "default"
"given" %||% "default"
#> [1] "given"
NA %||% "default"
#> [1] NA
character(0) %||% "default"
#> character(0)
list() %||% "default"
#> list()
NULL %||% NULL %||% 3
#> [1] 3

# Typical use: options and list fields that may be missing
options_given <- list(sep = ";")
options_given$sep %||% ","
#> [1] ";"
options_given$quote %||% "\""
#> [1] "\""

# The right side is evaluated lazily
res <- NULL
res %||% "alternative result"
#> [1] "alternative result"
x <- "kept" %||% stop("parsed, but not evaluated")
x
#> [1] "kept"
tryCatch(NULL %||% stop("evaluated now"), error = \(e) conditionMessage(e))
#> [1] "evaluated now"

# What it replaces
isbn <- NULL
if (is.null(isbn)) "unknown" else isbn
#> [1] "unknown"
isbn %||% "unknown"
#> [1] "unknown"

Three properties matter:

  • It tests only for NULL. NA, character(0), list() and FALSE are all kept, so NA %||% "default" is NA. For "missing or empty" write the test you mean.

  • The right side is evaluated lazily, like the right side of ||: "kept" %||% stop(...) never calls stop(), as the help page’s own example says ("parsed, but not evaluated"), and NULL %||% stop(...) does. It can therefore carry an expensive default or a failure: x <- cfg$path %||% stop("a path is required").

  • It is the natural way to read an optional field of a list, such as an option the caller may omit (list$name is NULL when the name is absent).

For years code defined its own %||% (often in utils.R, or imported from rlang or purrr); that still works, and the definition simply masks the base one, as User-defined operators shows. A package that wants the base operator must say Depends: R (>= 4.4.0) in its DESCRIPTION, and one that wants to run on older versions keeps its own helper.

Matrix operators and the colon

* on two matrices is elementwise. The matrix product is %*%, the outer product is %o% (which is outer()), and the Kronecker product is %x% (which is kronecker()); all three are special operators defined in base R by name, and they have the precedence of every %name% operator, which is higher than * and + (R documentation: matmult, R documentation: outer, R documentation: kronecker).

# Matrix product, outer product, Kronecker product
a <- matrix(1:4, nrow = 2L)
b <- matrix(5:8, nrow = 2L)
a
#>      [,1] [,2]
#> [1,]    1    3
#> [2,]    2    4
a * b
#>      [,1] [,2]
#> [1,]    5   21
#> [2,]   12   32
a %*% b
#>      [,1] [,2]
#> [1,]   23   31
#> [2,]   34   46
1:3 %*% 1:3
#>      [,1]
#> [1,]   14
1:3 %*% t(1:3)
#>      [,1] [,2] [,3]
#> [1,]    1    2    3
#> [2,]    2    4    6
#> [3,]    3    6    9
c(1, 2) %*% a
#>      [,1] [,2]
#> [1,]    5   11
a %*% c(1, 2)
#>      [,1]
#> [1,]    7
#> [2,]   10
crossprod(1:3)
#>      [,1]
#> [1,]   14

%*% treats a plain vector as a row or a column, whichever makes the product conformable, so 1:3 %*% 1:3 is the inner product 14 as a 1 by 1 matrix and 1:3 %*% t(1:3) is the 3 by 3 outer product. The help page says crossprod() and tcrossprod() are typically preferable for cross products, and options("matprod") controls how %*% treats NaN and Inf. A shape mismatch is an error, while the elementwise operators on a matrix and a vector follow recycling. Matrices and arrays are on Matrices and Arrays.

# The "wrong" shapes
tryCatch(a %*% 1:3, error = \(e) conditionMessage(e))
#> [1] "non-conformable arguments"
tryCatch(matrix(1:6, 2L) %*% matrix(1:6, 2L), error = \(e) conditionMessage(e))
#> [1] "non-conformable arguments"
tryCatch(a * 1:3, warning = \(w) conditionMessage(w))
#> [1] "longer object length is not a multiple of shorter object length"
tryCatch(a + matrix(1:6, 2L), error = \(e) conditionMessage(e))
#> [1] "non-conformable arrays"

non-conformable arguments is the %*% error (the number of columns of the left matrix differs from the number of rows of the right), and non-conformable arrays is the one for elementwise operators on arrays of different dimensions. A shorter vector combined with a matrix goes through the recycling rule and warns when the lengths do not divide.

# %o% is outer(): a multiplication table
1:3 %o% 1:4
#>      [,1] [,2] [,3] [,4]
#> [1,]    1    2    3    4
#> [2,]    2    4    6    8
#> [3,]    3    6    9   12
outer(1:3, 1:4, "+")
#>      [,1] [,2] [,3] [,4]
#> [1,]    2    3    4    5
#> [2,]    3    4    5    6
#> [3,]    4    5    6    7
dim(1:2 %o% matrix(1:4, 2L))
#> [1] 2 2 2

# %x% is kronecker()
diag(2L) %x% matrix(1, 2L, 2L)
#>      [,1] [,2] [,3] [,4]
#> [1,]    1    1    0    0
#> [2,]    1    1    0    0
#> [3,]    0    0    1    1
#> [4,]    0    0    1    1

The colon operator

from:to builds a sequence in steps of one, upwards or downwards (R documentation: Colon). For numbers it is equivalent to seq(from, to) with a step of 1 or -1, and the result is an integer vector when from is integer-valued and the result is representable as integers, a double vector otherwise; to is included if it differs from from by a whole number up to a fuzz of about 1e-7. On factors it means the interaction of the two. It is a binary operator with high precedence, higher than * and + but lower than ^ and unary minus, which is why 1:n - 1 is (1:n) - 1, not 1:(n - 1). Its two classic traps are that 1:0 is the descending sequence 1 0, not an empty one, and that it fails on NA with NA/NaN argument. When the endpoint comes from a variable that can be zero, use seq_len(n) or seq_along(x), which are empty for zero (see Control Flow).

# The colon operator builds integer sequences
1:5
#> [1] 1 2 3 4 5
5:1
#> [1] 5 4 3 2 1
2.5:6
#> [1] 2.5 3.5 4.5 5.5
-1:2
#> [1] -1  0  1  2
1:0
#> [1] 1 0
length(1:0)
#> [1] 2
class(1:3)
#> [1] "integer"
class(1.5:3.5)
#> [1] "numeric"
tryCatch(1:NA, error = \(e) conditionMessage(e))
#> [1] "NA/NaN argument"
tryCatch(1:c(2, 3), warning = \(w) conditionMessage(w))
#> [1] "numerical expression has 2 elements: only the first used"
seq_len(0L)
#> integer(0)
n <- 0L
for (i in seq_len(n)) print(i)

The last warning shows another trap: 1:c(2, 3) uses only the first element of the right side and says numerical expression has 2 elements: only the first used. In model formulae : means interaction instead (see Formulas).

Pipe, lambda, formula and help

Four operators are syntax more than arithmetic, and most of them have a page of their own; this section only places them in the precedence table.

The native pipe

lhs |> rhs passes the value of lhs as the first argument of the call rhs. It is a syntax transformation: the parser rewrites x |> f(y) as f(x, y) before anything is evaluated, so the pipe never exists as a function at run time (R documentation: pipeOp). The right side must be a call, and 1:3 |> sum is a syntax error whose text names the problem. The placeholder _ (R 4.2.0) can be used once as a named argument to pass the value somewhere else than first, and the NEWS of R 4.3.0 describes its use as the head of an extraction chain (_$coef[[2]]) as experimental. The pipe is a feature of R 4.1.0 (R NEWS: Changes in R 4.1.0); code using it does not parse on older versions.

source("scenario.R")

# |> is a syntax transformation: the parser rewrites it before evaluation
quote(books |> nrow())
#> nrow(books)
quote(books$year |> range(na.rm = TRUE))
#> range(books$year, na.rm = TRUE)
books$year |> range(na.rm = TRUE)
#> [1] 1813 2023
quote(1:3 |> rev())
#> rev(1:3)
quote(x |> f(y) |> g())
#> g(f(x, y))
tryCatch(
  parse(text = "1:3 |> sum"),
  error = \(e) conditionMessage(e)
)
#> [1] "The pipe operator requires a function call as RHS (<input>:1:8)"
1:3 |> (\(v) v + 1)()
#> [1] 2 3 4
books |> subset(year > 2020L, select = title) |> nrow()
#> [1] 3
books |> lm(year ~ pages, data = _) |> class()
#> [1] "lm"

The pipe’s precedence is that of the %name% operators, so the two forms x |> f() %in% y and x %in% y |> f() group differently, and x + y |> f() is x + f(y), and 2 * 3 |> sqrt() is 2 * sqrt(3); parenthesise the left side when it is an expression. The full treatment, including the lambda form (\(v) ...)() and the placeholder, is Pipes and the Native Pipe.

The lambda shorthand

R 4.1.0 added \(x) x + 1 as another way to write function(x) x + 1; the NEWS entry says it "is parsed as" the long form (R NEWS: Changes in R 4.1.0). The backslash starts a function definition, not an operator of the precedence table, and like function the body extends as far to the right as the expression allows. The help page of function says the shorthand form "is parsed as" the long one (R documentation: function).

# \(x) is function(x) in disguise
f <- \(x) x + 1
g <- function(x) x + 1
identical(body(f), body(g))
#> [1] TRUE
deparse(quote(\(x) x + 1))
#> [1] "function(x) x + 1"
class(quote(\(x) x + 1))
#> [1] "call"

The formula tilde

~ is unary or binary and does not evaluate its operands: y ~ x is a call to ~ that R keeps, together with the environment where it was written, as an object of class "formula" (R documentation: tilde). It has a very low precedence, below | and ||, so a formula can contain whole logical expressions on either side, and only the assignments and ? are lower. Inside a formula the usual operators change meaning (+ adds a term, : and * build interactions, | and %in% have special readings), which is the subject of Formulas.

# ~ builds a formula: an unevaluated call that keeps its environment
fml <- pages ~ year + genre
class(fml)
#> [1] "formula"
length(fml)
#> [1] 3
fml[[1L]]
#> `~`
fml[[2L]]
#> pages
fml[[3L]]
#> year + genre
length(~ year)
#> [1] 2
all.vars(fml)
#> [1] "pages" "year"  "genre"
is.call(fml)
#> [1] TRUE
identical(environment(fml), environment())
#> [1] TRUE

The help operator

? is the lowest-precedence operator of the table, unary (?mean) or binary (type?topic, such as methods?mean); it is documented in the utils package (R documentation: Question). In the ?Syntax note, S gave ? the same precedence as binary + and -, while in R it is the lowest, which is why ?x + y asks for help on the result of x + y. The example only parses expressions with quote(), because running ?mean in a script would print a help page. See Getting Help and Documentation.

# ? is an operator too, unary and binary; here it is only parsed, not run
quote(?mean)
#> `?`(mean)
quote(?"[")
#> `?`("[")
quote(methods ? mean)
#> `?`(methods, mean)
as.list(quote(?mean))
#> [[1]]
#> `?`
#>
#> [[2]]
#> mean
#>

User-defined operators

Any function whose name starts and ends with % can be written between its two arguments. The R Language Definition says that "R allows user-defined infix operators. These have the form of a string of characters delimited by the % character", that the string “can contain any printable character except %”, and that the escape sequences for strings do not apply (The R Language Definition: Special operators). An Introduction to R makes the same point from the function side: a function named %anything% "could have been used as a binary operator in expressions rather than in function form", and the name has to be quoted when it is assigned, because it is not a syntactic name (An Introduction to R: Defining new binary operators).

source("scenario.R")

# Any name between two percent signs is a binary operator
`%+%` <- function(lhs, rhs) paste0(lhs, rhs)
"Dune" %+% " (" %+% 1965L %+% ")"
#> [1] "Dune (1965)"

"%||%" <- function(x, y) if (is.null(x)) y else x
NULL %||% "own version"
#> [1] "own version"

# Operators are looked up like any other function
`%between%` <- function(x, range) x >= range[[1L]] & x <= range[[2L]]
books$year %between% c(2010L, 2020L)
#>  [1] FALSE FALSE  TRUE FALSE  TRUE FALSE    NA FALSE  TRUE FALSE FALSE  TRUE
#> [13]  TRUE  TRUE FALSE  TRUE
sum(books$year %between% c(2010L, 2020L), na.rm = TRUE)
#> [1] 7

# Any characters except a percent sign are allowed in the name
`%!%` <- function(x, y) x & !y
c(TRUE, TRUE) %!% c(TRUE, FALSE)
#> [1] FALSE  TRUE
`%is%` <- function(x, class) inherits(x, class)
books %is% "data.frame"
#> [1] TRUE

The first chunk defines %+% as paste0(). Because the operator is only a function, the definition of %||% further down in the script masks the base operator for the rest of the session: lookup finds the global definition first (see Scoping, Environments and Closures). That is how code written before R 4.4.0 keeps working, and it is why such helpers should not be exported from a package under the same name as a base function without a reason.

What you can and cannot choose

  • The name is yours: %between%, %is%, %!% and %+% above are all legal. The characters between the percent signs can be anything printable except %, but a readable name (letters, or one or two symbols) is what makes the operator worth having, and the backslash is the one character An Introduction to R calls "not a convenient choice".

  • The precedence is not yours. Every %name% operator, together with the pipe |>, sits at one level of the table: higher than * and /, lower than : and unary minus, and left to right. Whatever your function means, 2 * 3 %tt% 4 is 2 * (3 %tt% 4).

  • The operator is binary only. With one argument it has to be called as a function, with the name in backticks.

  • The percent sign inside a string is not an operator ("100%"), and sprintf() uses %% for a literal percent sign. A name that was never defined is the usual error: could not find function "%nope%".

# Precedence is fixed: all %op% share one level, and group left to right
`%tt%` <- function(x, y) x * 10 + y
1 %tt% 2 %tt% 3
#> [1] 123
2 * 3 %tt% 4
#> [1] 68
-1 %tt% 2
#> [1] -8
2^3 %tt% 4
#> [1] 84
1:3 %tt% 4
#> [1] 14 24 34

# The operator is an ordinary function: it can be called, passed, listed
`%+%`("a", "b")
#> [1] "ab"
vapply(c("a", "b"), `%+%`, character(1L), "!")
#>    a    b
#> "a!" "b!"
Reduce(`%+%`, c("a", "b", "c"))
#> [1] "abc"
Map(`%tt%`, 1:2, 3:4)
#> [[1]]
#> [1] 13
#>
#> [[2]]
#> [1] 24
#>
exists("%between%")
#> [1] TRUE
is.function(`%between%`)
#> [1] TRUE
class(quote(1 %tt% 2))
#> [1] "call"
quote(1 %tt% 2)[[1L]]
#> `%tt%`

# Undefined operators and the percent sign inside text
tryCatch(1 %nope% 2, error = \(e) conditionMessage(e))
#> [1] "could not find function \"%nope%\""
"100%"
#> [1] "100%"
sprintf("%d%%", 50L)
#> [1] "50%"

Operators are ordinary function values, so they can be given to the functional tools: vapply() over a character vector with the operator and an extra argument passes it on, Reduce() folds a vector with the operator and Map() applies it to pairs (see The apply Family and Functionals). The R Language Definition adds that the predefined operators are %% %*% %/% %in% %o% %x% %||%; the manual of R 4.6.1 does not list %notin% yet, which was added in R 4.6.0.

When should you define one? An operator pays for itself when an expression is read far more often than written and a name between its arguments reads better than a call, as books$year %between% c(2010L, 2020L) does. It does not when a function with named arguments would document itself, or when it hides a side effect; and since the precedence is fixed, a custom operator inside a longer expression often needs the parentheses that a call would not.

Overriding operators with S3

The built-in arithmetic, comparison and logical operators are generic: the help page for the group generics lists the members of the Ops group as + - * / ^ %% %/%, & | ! and == != < <= >= >, and for them an S3 method for the class of either operand replaces the internal code (R documentation: groupGeneric). A method can be written for one operator (==.book) or for the whole group (Ops.book); a specific method wins over the group one, the name of the operator is in .Generic, and a unary operator calls the method with e2 missing. The shelf book class has no operators, so b1 == b2 fails and b1 + 1 fails with the same message as for any non-numeric object.

source("scenario.R")

# The operators are generic: an S3 method for a class changes what they do
b1 <- book("Dune", "Frank Herbert", "978-0-306-40615-7", 1965L)
b2 <- book("Dune (second copy)", "Frank Herbert", "9780306406157", 1965L)
tryCatch(b1 == b2, error = \(e) conditionMessage(e))
#> [1] "comparison of these types is not implemented"
tryCatch(b1 + 1, error = \(e) conditionMessage(e))
#> [1] "non-numeric argument to binary operator"

`==.book` <- function(e1, e2) {
  e1$isbn == e2$isbn
}
`!=.book` <- function(e1, e2) {
  !(e1 == e2)
}
b1 == b2
#> [1] TRUE
b1 != b2
#> [1] FALSE

# A method for the Ops group generic covers many operators at once
Ops.book <- function(e1, e2) {
  op <- .Generic # nolint: object_usage_linter.
  if (op %in% c("<", ">", "<=", ">=")) {
    get(op)(e1$year, e2$year)
  } else {
    stop("operator ", op, " is not defined for books", call. = FALSE)
  }
}
b3 <- book("Emma", "Jane Austen", "978-0-14-143951-8", 1815L)
b3 < b1
#> [1] TRUE
b1 >= b3
#> [1] TRUE
tryCatch(b1 * 2, error = \(e) conditionMessage(e))
#> [1] "operator * is not defined for books"
# A specific method wins over the group method
b1 == b2
#> [1] TRUE

# Unary operators have e2 missing
`-.book` <- function(e1, e2) {
  if (missing(e2)) "unary minus" else "binary minus"
}
-b1
#> [1] "unary minus"
b1 - b3
#> [1] "binary minus"

Points from the help page that matter in practice:

  • The classes of both arguments are considered. If a method is found for only one argument, or the same method for both, it is used. If different methods are found, R calls the generic chooseOpsMethod() (new in R 4.3.0) to pick one, and when that does not resolve the conflict there is a warning about "incompatible methods" and the internal operator is used.

  • && and || are not members of Ops: they cannot be overloaded, and they always require TRUE/FALSE-like single values (see The length-1 errors of && and ||). Likewise %in% is an ordinary function that calls match(); to change how objects of a class are matched, provide an mtfrm() method, as the match help page describes.

  • The matrix product %*% is not in Ops: since R 4.3.0 it is an S3 generic of its own group, matrixOps.

  • A user-defined %op% operator is not generic. If you want per-class behaviour, make it one with UseMethod(), as the last chunk does. A method named %+%.book is found like any generic.class method.

Writing complete, well-behaved operator methods (keeping attributes, checking the class of e2, NextMethod()) and the S4 equivalents are covered in S3 Group Generics and Operator Overloading and S4 Classes and Methods.

# User-defined %op% operators dispatch only if a generic is written
`%+%` <- function(x, y) UseMethod("%+%")
`%+%.default` <- function(x, y) { # nolint: object_name_linter.
  paste0(x, y)
}
`%+%.book` <- function(x, y) { # nolint: object_name_linter.
  paste(x$title, "and", y$title)
}
"a" %+% "b"
#> [1] "ab"
b1 %+% b3
#> [1] "Dune and Emma"

Precedence and associativity

R decides how to read a + b * c from a table of precedence levels. The table is in two official places: the Details section of ?Syntax, "from highest to lowest", and the section "Infix and prefix operators" of the R Language Definition (The R Language Definition: Infix and prefix operators). The two agree where they overlap. The help page also lists the indexing operators [ and [[ and the help operator ?, which the manual’s list omits, and the manual states the two rules about direction that the help page gives only in a sentence: within one level operators evaluate left to right, except for exponentiation and the left assignments.

The following table reproduces the table of the Syntax help page, with two added columns: the level number and how the operators at that level group.

Level Operators Meaning (as in Syntax) Grouping

1 (highest)

:: :::

access variables in a namespace

left to right

2

$ @

component and slot extraction

left to right

3

[ [[

indexing

left to right

4

^

exponentiation

right to left

5

- + (unary)

unary minus and plus

prefix

6

:

sequence operator

left to right

7

%any%, |>

special operators, including %% and %/%, and the native pipe

left to right

8

* /

multiply, divide

left to right

9

+ - (binary)

add, subtract

left to right

10

< > <= >= == !=

ordering and comparison

not chainable (see below)

11

!

negation

prefix

12

& &&

and

left to right

13

| ||

or

left to right

14

~

as in formulae

left to right

15

-> ->>

rightwards assignment

left to right

16

<- <<-

assignment

right to left

17

=

assignment

right to left

18 (lowest)

?

help (unary and binary)

left to right

The notes that go with the table:

  • Operators of equal precedence are evaluated from left to right "except where indicated", and the help page adds "(Note that = is not necessarily an operator.)": the = that names an argument in f(x = 1) is syntax of the call, and only = as an assignment at the top of an expression is at level 17.

  • The binary operators ::, :::, $ and @ require names or string constants on the right-hand side, and the first two also on the left.

  • The manual spells out the consequences for a few expressions: : precedes binary + and -, but not ^, so 1:3-1 is 0 1 2 while 1:2^3 is 1:8; 2 ^ 2 ^ 3 is 2 ^ 8, not 4 ^ 3, whereas 1 - 1 - 1 is -1, not 1; and "the operators %% and %/% for integer remainder and divide have higher precedence than multiply and divide".

  • The manual’s list says that "all other operators group left to right". The comparison operators are the exception that the parser enforces: they are left to right in the table but cannot be chained at all, as the next section shows.

  • The table covers operators, not keywords: function, \(x), if, for, while and repeat take an expression as their body, and that body extends as far to the right as the expression can, so \(x) x + 1 * 2 is a function whose body is x + 1 * 2.

  • The pipe is at the level of %any%, and the parser removes it: x |> f(y) is already f(x, y) when the evaluator sees it (see The native pipe).

  • The help page’s Note says that there are substantial precedence differences between R and S: in S, ? has the same precedence as binary + and -, and &, &&, | and || have equal precedence.

Reading a precedence level

The best way to learn the table is to ask R to show the grouping. The helper below walks the parse tree of an expression (see Language Objects and Quoting) and writes every operator call with its own parentheses; it is a teaching aid, not something to import. Each of the following lines is show_grouping() of the text on its left.

# Show how R groups an expression by writing every operator call in parentheses
parenthesise <- function(x) {
  if (!is.call(x)) {
    return(paste(deparse(x), collapse = " "))
  }
  head <- x[[1L]]
  args <- as.list(x)[-1L]
  if (!is.name(head)) {
    return(paste(deparse(x), collapse = " "))
  }
  op <- as.character(head)
  parts <- vapply(args, parenthesise, character(1L))
  is_binary_op <- op %in% c(
    "::", ":::", "$", "@", "^", ":", "*", "/", "+", "-", "<", ">", "<=", ">=",
    "==", "!=", "&", "&&", "|", "||", "~", "<-", "<<-", "=", "?"
  ) || startsWith(op, "%")
  if (op == "[" || op == "[[") {
    paste0(parts[[1L]], op, paste(parts[-1L], collapse = ", "),
           if (op == "[") "]" else "]]")
  } else if (op == "function") {
    paste0("function(", paste(names(args[[1L]]), collapse = ", "), ") ",
           parts[[2L]])
  } else if (is_binary_op && length(parts) == 2L) {
    paste0("(", parts[[1L]], " ", op, " ", parts[[2L]], ")")
  } else if (op %in% c("-", "+", "!", "~", "?") && length(parts) == 1L) {
    paste0("(", op, parts[[1L]], ")")
  } else if (op == "(") {
    paste0("(", parts[[1L]], ")")
  } else {
    paste0(op, "(", paste(parts, collapse = ", "), ")")
  }
}

show_grouping <- function(text) {
  cat(sprintf("%-18s %s\n", text, parenthesise(str2lang(text))))
}
# Highest to lowest, one expression per level
show_grouping("a::b$c")
#> a::b$c             ((a :: b) $ c)
show_grouping("x$y[1]^2")
#> x$y[1]^2           ((x $ y)[1] ^ 2)
show_grouping("x[1]^2")
#> x[1]^2             (x[1] ^ 2)
show_grouping("-a^b")
#> -a^b               (-(a ^ b))
show_grouping("-a:b")
#> -a:b               ((-a) : b)
show_grouping("a:b %o% c")
#> a:b %o% c          ((a : b) %o% c)
show_grouping("a %o% b * c")
#> a %o% b * c        ((a %o% b) * c)
show_grouping("a * b + c")
#> a * b + c          ((a * b) + c)
show_grouping("a + b < c")
#> a + b < c          ((a + b) < c)
show_grouping("a < b & !c")
#> a < b & !c         ((a < b) & (!c))
show_grouping("a & b | c")
#> a & b | c          ((a & b) | c)
show_grouping("a | b ~ c")
#> a | b ~ c          ((a | b) ~ c)
show_grouping("a ~ b -> c")
#> a ~ b -> c         (c <- (a ~ b))
show_grouping("x <- y = 5")
#> x <- y = 5         ((x <- y) = 5)
show_grouping("x = y <- 5")
#> x = y <- 5         (x = (y <- 5))

The first chunk follows the table from the top: the extraction operator binds tightest (a::b$c is (a::b)$c), indexing and ^ follow, then unary minus, :, the %name% family, multiplication, addition, comparison, !, &, |, the formula tilde, and the assignments. Two lines deserve a second look. a ~ b -> c is read as c <- (a ~ b): the right assignment is parsed first as a call to <- with the sides swapped, which is why -> appears in the output as <-. And x <- y = 5 is (x <- y) = 5, because = has a lower precedence than <-; the Syntax help page uses that example to explain why the two assignments should not be mixed.

# Associativity: ^ and the left assignments group right to left
show_grouping("2^3^2")
#> 2^3^2              (2 ^ (3 ^ 2))
show_grouping("1 - 1 - 1")
#> 1 - 1 - 1          ((1 - 1) - 1)
show_grouping("a %o% b %o% c")
#> a %o% b %o% c      ((a %o% b) %o% c)
show_grouping("a <- b <- c")
#> a <- b <- c        (a <- (b <- c))
show_grouping("a = b = c")
#> a = b = c          (a = (b = c))
show_grouping("a -> b -> c")
#> a -> b -> c        (c <- (b <- a))
show_grouping("a <- b -> c")
#> a <- b -> c        (a <- (c <- b))

The direction is the other thing the table tells you. ^, <- and = group from right to left: 2^3^2 is 2^(3^2), and a <- b <- c assigns c to both. Everything else, including -, /, the %name% operators and, perhaps unexpectedly, ->, groups from left to right: a -> b -> c is (a -> b) -> c, which assigns a to b and then b to c.

# Unary minus versus ^, : and %%
show_grouping("-2^2")
#> -2^2               (-(2 ^ 2))
show_grouping("2^-2")
#> 2^-2               (2 ^ (-2))
show_grouping("-2:2")
#> -2:2               ((-2) : 2)
show_grouping("-5 %/% 2")
#> -5 %/% 2           ((-5) %/% 2)
show_grouping("1:n-1")
#> 1:n-1              ((1 : n) - 1)
show_grouping("1:2^3")
#> 1:2^3              (1 : (2 ^ 3))

# ! and the comparison and logical operators
show_grouping("!x %in% y")
#> !x %in% y          (!(x %in% y))
show_grouping("!x == y")
#> !x == y            (!(x == y))
show_grouping("!x & y")
#> !x & y             ((!x) & y)
show_grouping("a || b && c")
#> a || b && c        (a || (b && c))
show_grouping("!!x")
#> !!x                (!(!x))
show_grouping("--x")
#> --x                (-(-x))

# The pipe is rewritten by the parser, at the level of %any%
show_grouping("x |> f() |> g()")
#> x |> f() |> g()    g(f(x))
show_grouping("x %in% y |> f()")
#> x %in% y |> f()    f((x %in% y))
show_grouping("x |> f() %in% y")
#> x |> f() %in% y    (f(x) %in% y)
show_grouping("x + y |> f()")
#> x + y |> f()       (x + f(y))
show_grouping("x * y |> f()")
#> x * y |> f()       (x * f(y))

# A lambda or function body reaches as far to the right as it can
show_grouping("\\(x) x + 1 * 2")
#> \(x) x + 1 * 2     function(x) (x + (1 * 2))
show_grouping("function(x) x ~ y")
#> function(x) x ~ y  function(x) (x ~ y)
show_grouping("?x + y")
#> ?x + y             (?(x + y))
show_grouping("a ? b ? c")
#> a ? b ? c          ((a ? b) ? c)

The remaining chunks show the places where a precedence you did not expect changes the answer: -2^2 is -(2^2), but 2^-2 is allowed and means 2^(-2); -2:2 is (-2):2; -5 %/% 2 is (-5) %/% 2; and !x %in% y is !(x %in% y) while !x & y is (!x) & y. The pipe shows its level in the last lines: x + y |> f() is x + f(y), and x %in% y |> f() is f(x %in% y), since the %name% operators and the pipe share a level and group from left to right.

Precedence traps

Each of the following lines is correct R that does something other than what many readers expect. The #> line under each expression is the answer; the version with parentheses is the one to write when the reader should not have to know the table.

# Unary minus binds looser than ^ but tighter than : and %%
-2^2
#> [1] -4
(-2)^2
#> [1] 4
2^-1
#> [1] 0.5
-1:3
#> [1] -1  0  1  2  3
-(1:3)
#> [1] -1 -2 -3
-5 %/% 2
#> [1] -3
-(5 %/% 2)
#> [1] -2

# : binds tighter than binary + and -, but not tighter than ^
n <- 5L
1:n - 1L
#> [1] 0 1 2 3 4
1:(n - 1L)
#> [1] 1 2 3 4
1:3 + 1
#> [1] 2 3 4
1:(3 + 1)
#> [1] 1 2 3 4
1:2^3
#> [1] 1 2 3 4 5 6 7 8
(1:2)^3
#> [1] 1 8

# %% and %/% bind tighter than * and /
10 - 2 * 7 %% 4
#> [1] 4
10 - 2 * (7 %% 4)
#> [1] 4
5 %/% 2 * 2
#> [1] 4
5 %/% (2 * 2)
#> [1] 1
8 / 4 %/% 2
#> [1] 4
  • -2^2 is -4: the exponent binds tighter than the minus sign, as it does in mathematics. (-2)^2 is 4.

  • -1:3 is -1 0 1 2 3, the sequence from -1; -(1:3) negates the sequence. -5 %/% 2 is -3 because the minus applies to the 5 first.

  • 1:n - 1 is (1:n) - 1, the sequence from 0 to n - 1. This is the most frequent precedence bug in R code: the sequence you meant is 1:(n - 1), and for a variable length seq_len(n - 1). In the same way 1:2^3 is 1:8, not (1:2)^3.

  • %% and %/% bind tighter than *: 10 - 2 * 7 %% 4 is 10 - 2 * 3. 5 %/% 2 * 2 is (5 %/% 2) * 2, which is 4, not the 1 that 5 %/% (2 * 2) gives.

# Right-to-left: ^ and the left assignments
2^3^2
#> [1] 512
(2^3)^2
#> [1] 64
100 / 10 / 2
#> [1] 5
100 / (10 / 2)
#> [1] 20
a <- b <- 5L
c(a, b)
#> [1] 5 5
10 -> d # nolint: assignment_linter.
d
#> [1] 10

# ! binds looser than comparison and %in%, tighter than & and |
x <- 2L
!x > 3L
#> [1] TRUE
!(x > 3L)
#> [1] TRUE
(!x) > 3L
#> [1] FALSE
!x %in% 1:3
#> [1] FALSE
TRUE || TRUE && FALSE
#> [1] TRUE
(TRUE || TRUE) && FALSE
#> [1] FALSE
TRUE | FALSE & FALSE
#> [1] TRUE
FALSE & TRUE | TRUE
#> [1] TRUE
FALSE & (TRUE | TRUE)
#> [1] FALSE
  • 2^3^2 is 512: the exponent groups to the right. 100 / 10 / 2 is 5, grouped to the left.

  • !x > 3L is !(x > 3L), so with x <- 2L it is TRUE; (!x) > 3L is FALSE > 3L. Negation is lower than comparison and than %in%. It is higher than & and |, so !x & y negates only x.

  • TRUE || TRUE && FALSE is TRUE || (TRUE && FALSE), which is TRUE, while (TRUE || TRUE) && FALSE is FALSE. The same with the elementwise forms: TRUE | FALSE & FALSE is TRUE. The AND operators bind tighter, as in logic, but not as in S.

# Comparison operators do not chain
tryCatch(parse(text = "1 < 2 < 3"), error = \(e) conditionMessage(e))
#> [1] "<text>:1:7: unexpected '<'\n1: 1 < 2 <\n          ^"
tryCatch(parse(text = "a == b == c"), error = \(e) conditionMessage(e))
#> [1] "<text>:1:8: unexpected '=='\n1: a == b ==\n           ^"
(1 < 2) == (2 < 3)
#> [1] TRUE
(1 < x) & (x < 10)
#> [1] TRUE

# An assignment on the left of = is a different story
tryCatch(eval(parse(text = "x <- y = 5")), error = \(e) conditionMessage(e))
#> [1] "could not find function \"<-<-\""
tryCatch(eval(parse(text = "2 + 2 <- 5")), error = \(e) conditionMessage(e))
#> [1] "target of assignment expands to non-language object"
x = y <- 5L # nolint: assignment_linter.
c(x, y)
#> [1] 5 5
  • Comparison operators do not chain. 1 < 2 < 3 is not read as (1 < 2) < 3 but rejected by the parser: unexpected '<'. The same for a == b == c. Write 1 < x & x < 10, or (1 < x) & (x < 10); findInterval() and cut() handle many break points at once. The manual’s sentence that all other operators group left to right is not the whole story here.

  • x <- y = 5 is the one precedence error that gets past the parser: the assignment <- needs a name on its left, but (x <- y) is a call, so R looks for a function called <-<- and fails with could not find function "<-<-". The Syntax help page describes this as a "non-catchable" error in its Not run example. 2 + 2 <- 5 parses ((2 + 2) <- 5), and fails when evaluated with target of assignment expands to non-language object. The valid chains are x <- y <- 5 and x = y <- 5.

# Operators and the pipe: the pipe's left side is everything at a higher level
c(1, 4, 9) |> sqrt() |> sum()
#> [1] 6
sum(1, 2) |> (\(s) s * 2)()
#> [1] 6
quote(a + b |> f())
#> a + f(b)
2 * 3 |> sqrt()
#> [1] 3.464102
(2 * 3) |> sqrt()
#> [1] 2.44949
  • The pipe takes everything at a higher level on its left as the left-hand side, and the operand to its right is a call: 2 * 3 |> sqrt() is 2 * sqrt(3), which is about 3.46, not sqrt(6); write (2 * 3) |> sqrt(). a + b |> f() is a + f(b).

To avoid all of these, use parentheses wherever the reader must know the table to be sure, and use the named helpers (seq_len(), isTRUE(), any()) instead of expressions that need one. Linters such as lintr flag some of the common mistakes (see Style, Linting and Static Analysis).

Exact equality with identical()

== returns a vector with the same shape as its operands, and it returns NA when an operand is NA; it is a comparison of values, not a test of whether two objects are the same. For the single TRUE or FALSE that if, while and && need, R has identical(). Its help page opens with that advice: “A call to identical is the way to test exact equality in if and while statements, as well as in logical expressions that use && or ||”, because == “returns an object like the arguments”, and if one argument is NA “the result is also NA” (R documentation: identical). The value of identical() is always one logical value, never NA, and it never signals an error.

source("scenario.R")

# identical(): exact equality, always one TRUE or FALSE
identical(1L, 1L)
#> [1] TRUE
identical(1L, 1)
#> [1] FALSE
1L == 1
#> [1] TRUE
identical(c(a = 1), 1)
#> [1] FALSE
c(a = 1) == 1
#>    a
#> TRUE
identical(NULL, c())
#> [1] TRUE
identical(NA, NA_integer_)
#> [1] FALSE
identical(NA_real_, NaN)
#> [1] FALSE
identical(NaN, NaN)
#> [1] TRUE
identical(0, -0)
#> [1] TRUE
identical(0, -0, num.eq = FALSE)
#> [1] FALSE
identical(list(1, "a"), list(1, "a"))
#> [1] TRUE
identical(NA, 1)
#> [1] FALSE
tryCatch(
  if (NA == 1) "yes", # nolint: equals_na_linter.
  error = \(e) conditionMessage(e)
)
#> [1] "missing value where TRUE/FALSE needed"
if (identical(NA, 1)) "yes" else "no"
#> [1] "no"
tryCatch(if (logical(0)) "yes", error = \(e) conditionMessage(e))
#> [1] "argument is of length zero"

What identical() compares is the whole object, including its type and attributes:

  • identical(1L, 1) is FALSE while 1L == 1 is TRUE: an integer and a double are different types. identical(c(a = 1), 1) is FALSE: the names are an attribute.

  • NA is not NA_integer_; with the default single.NA = TRUE there is one NA and one NaN per type (so NaN is identical to NaN, but NA_real_ is not identical to NaN).

  • 0 and -0 are identical by default (num.eq = TRUE compares numbers with ==); num.eq = FALSE compares the bits and tells them apart.

  • Attributes are compared as a set by default (attrib.as.set = TRUE), so their order does not matter; attrib.as.set = FALSE makes it matter.

  • identical(NULL, c()) is TRUE, because c() returns NULL.

The other arguments of identical() are ignore.bytecode, ignore.environment, ignore.srcref and extptr.as.ref; the strictest test the help page documents is identical(x, y, num.eq = FALSE, single.NA = FALSE, attrib.as.set = FALSE, ignore.bytecode = FALSE, ignore.environment = FALSE, ignore.srcref = FALSE, extptr.as.ref = TRUE). Closures are compared by formals, body and environment (the bytecode and source references are ignored by default), so two closures made by the same factory are not identical unless ignore.environment = TRUE. The help page also warns that you almost never want identical() on date-times of class "POSIXlt", because the same instant has several representations.

# Whole objects: the data frame equals a fresh copy of itself
identical(books, read.csv("books.csv"))
#> [1] TRUE
changed <- books
changed$year[2L] <- 1814L
identical(books, changed)
#> [1] FALSE
x <- structure(1:2, a = "p", b = "q")
y <- structure(1:2, b = "q", a = "p")
identical(x, y)
#> [1] TRUE
identical(x, y, attrib.as.set = FALSE)
#> [1] FALSE

# Functions: bytecode and source references are ignored by default
f1 <- function(x) x + 1
f2 <- function(x) x + 1
identical(f1, f2)
#> [1] TRUE
identical(f1, function(x) x + 2)
#> [1] FALSE
make_adder <- function(n) function(x) x + n
identical(make_adder(1), make_adder(1))
#> [1] FALSE
identical(make_adder(1), make_adder(1), ignore.environment = TRUE)
#> [1] TRUE

if (NA == 1) is the error missing value where TRUE/FALSE needed, and a zero-length condition is argument is of length zero; identical() never fails like that, but it also cannot say "unknown", so use it only where a plain yes or no is the right answer.

Approximate equality with all.equal()

all.equal(target, current, ...) is a generic function that compares two objects "with a numeric tolerance", and returns TRUE or a character vector that describes the differences (R documentation: all.equal). The default tolerance is sqrt(.Machine$double.eps), about 1.5e-8, and the R FAQ describes it as a numeric tolerance of .Machine$double.eps ^ 0.5 (R FAQ 7.31).

# all.equal(): "nearly equal", and a description of the difference otherwise
all.equal(1, 1 + 1e-10)
#> [1] TRUE
all.equal(1, 1.1)
#> [1] "Mean relative difference: 0.1"
all.equal(1L, 1)
#> [1] TRUE
all.equal(c(a = 1), c(b = 1))
#> [1] "Names: 1 string mismatch"
all.equal(1, "1")
#> [1] "Modes: numeric, character"
#> [2] "target is numeric, current is character"
all.equal(c("a", "b"), c("a", "c"))
#> [1] "1 string mismatch"
all.equal(1:3, c(1, 2, 4))
#> [1] "Mean relative difference: 0.3333333"
all.equal(books$year, changed$year)
#> [1] "Mean relative difference: 0.000551572"
all.equal(books, changed)
#> [1] "Component “year”: Mean relative difference: 0.000551572"
all.equal(1, 1.1, tolerance = 0.2)
#> [1] TRUE

The messages are descriptions for a human: Mean relative difference: 0.1, Names: 1 string mismatch, Modes: numeric, character, and for a data frame or list the component is named (Component “year”: ...). The result is TRUE when there is no difference and a character vector otherwise, so it cannot be used directly as a condition. The documented idiom is isTRUE(all.equal(x, y)), or stopifnot(), which accepts the result of all.equal() directly and shows its message in its own error.

# all.equal() does not return FALSE, so it cannot be used as a condition
isTRUE(all.equal(1, 1.1))
#> [1] FALSE
tryCatch(if (all.equal(1, 1.1)) "same", error = \(e) conditionMessage(e))
#> [1] "argument is not interpretable as logical"
if (isTRUE(all.equal(1, 1 + 1e-10))) "same" else "different"
#> [1] "same"

In tests, stopifnot() takes the result of all.equal() as it is, and its error message includes the description of the difference:

# In tests, stopifnot() accepts all.equal() directly
stopifnot(all.equal(0.1 + 0.2, 0.3))
tryCatch(
  stopifnot(all.equal(1, 1.1)),
  error = \(e) conditionMessage(e)
)
#> [1] "1 and 1.1 are not equal:\n  Mean relative difference: 0.1"

How the tolerance works

For numbers, all.equal.numeric() computes the mean absolute difference of the elements that differ, and divides it by the mean absolute value of target when that scale is finite and larger than the tolerance; otherwise the difference stays absolute. In the help page’s words, "differences are expressed relative to it; otherwise, absolute differences are used". Two consequences:

  • A target near zero is compared on an absolute scale, so all.equal(1e-10, 2e-10) is TRUE, although the numbers differ by a factor of two; tolerance = 0 shows the relative difference. If you compare tiny numbers, choose the tolerance or the scale yourself.

  • The mean is taken over the elements that differ, not over all of them, unless countEQ = TRUE (so one bad element in a long vector looks big).

# Arguments of all.equal.numeric
all.equal(100, 101)
#> [1] "Mean relative difference: 0.01"
all.equal(100, 101, scale = 1)
#> [1] "Mean absolute difference: 1"
all.equal(1e-10, 2e-10)
#> [1] TRUE
all.equal(1e-10, 2e-10, tolerance = 0)
#> [1] "Mean relative difference: 1"
all.equal(c(1, 2, 3), c(1, 2, 3.5), countEQ = TRUE)
#> [1] "Mean relative difference: 0.1111111"
all.equal(c(1, 2, 3), c(1, 2, 3.5))
#> [1] "Mean relative difference: 0.1666667"
all.equal(c(a = 1), c(b = 1), check.attributes = FALSE)
#> [1] TRUE
all.equal(c(a = 1, b = 2), c(a = 1, b = 2.0000001))
#> [1] "Mean relative difference: 5e-08"
all.equal(c(a = 1, b = 2), c(a = 1, b = 2.1))
#> [1] "Mean relative difference: 0.05"

The arguments of the numeric method are tolerance, scale (a number: absolute differences are scaled by it), countEQ, formatFUN, check.attributes, check.class and giveErr (which returns the numeric error as an attribute). The generic passes further arguments through ..., and arguments after the dots must be named in full. Other methods exist for character vectors, lists, environments (evaluate, all.names), functions (check.environment) and date-times (a tolerance of 0.001 seconds by default, check.tzone); attr.all.equal() compares attributes.

Changes in all.equal

Some behaviours that changed in the last releases matter when a test suite is moved between versions: R 4.1.0 made all.equal(f, g) on functions also compare their environments (use check.environment = FALSE for objects such as nls() fits), R 4.2.0 added a sanity check on the tolerance argument of all.equal.numeric() because all.equal(a, b, c) for three numeric vectors is "a surprisingly common error", R 4.3.0 added the giveErr switch, and R 4.4.0 gave the default and numeric methods a check.class argument (R NEWS).

See Testing with testthat for the testthat expectations that compare values with a tolerance or exactly.

Floating-point comparisons

R has no decimal type. A number in a numeric vector is an IEEE 754 double, and the R FAQ states the consequence: the only numbers a double holds exactly are integers and fractions whose denominator is a power of 2, every other number is rounded to (typically) 53 binary digits, and so two floating-point numbers are not reliably equal unless they were computed by the same algorithm, and not always even then (R FAQ 7.31). The first chunk reproduces the FAQ’s two examples with the real output and adds the usual third.

# FAQ 7.31: numbers that are not exactly representable
a <- sqrt(2)
a * a == 2
#> [1] FALSE
a * a - 2
#> [1] 4.440892e-16
print(a * a, digits = 18)
#> [1] 2.00000000000000044
.3 + .6 == .9
#> [1] FALSE
.3 + .6 - .9
#> [1] -1.110223e-16
0.1 + 0.2 == 0.3
#> [1] FALSE
print(c(0.1 + 0.2, 0.3), digits = 17)
#> [1] 0.30000000000000004 0.29999999999999999
identical(0.1 + 0.2, 0.3)
#> [1] FALSE
isTRUE(all.equal(0.1 + 0.2, 0.3))
#> [1] TRUE

The printed value 0.3 hides the difference, because print() shows seven significant digits by default (options(digits = 7)); digits = 17 shows the double that is really stored. 0.1 + 0.2 is 0.30000000000000004 and 0.3 is stored as 0.29999999999999999, which are two different doubles one unit in the last place apart.

Machine precision

.Machine$double.eps is the smallest positive number x for which 1 + x != 1, about 2.22e-16 (R documentation: .Machine). It is the spacing of doubles near 1, so relative comparisons are meaningful down to about that size, and sqrt(.Machine$double.eps) is the default tolerance of all.equal(). Two further lines in the output are worth a look: 2^53 + 1 == 2^53 is TRUE because doubles run out of integer precision at 2^53 (about 9.0e15), and sqrt(.Machine$double.eps) == 1.490116e-08 is FALSE because the typed constant is the printed, rounded value.

# Machine precision
.Machine$double.eps
#> [1] 2.220446e-16
.Machine$double.eps^0.5
#> [1] 1.490116e-08
1 + .Machine$double.eps == 1
#> [1] FALSE
1 + .Machine$double.eps / 2 == 1
#> [1] TRUE
sqrt(.Machine$double.eps) == 1.490116e-08
#> [1] FALSE
2^53 + 1 == 2^53
#> [1] TRUE
0.5 - 0.3 == 0.3 - 0.1
#> [1] FALSE
isTRUE(all.equal(0.5 - 0.3, 0.3 - 0.1))
#> [1] TRUE

# Exactly representable numbers: integers and fractions with a power of 2
0.5 + 0.25 == 0.75
#> [1] TRUE
0.1 * 3 == 0.3
#> [1] FALSE
3 / 10 == 0.3
#> [1] TRUE
sum(rep(0.1, 3L)) == 0.3
#> [1] FALSE

The last two chunk lines show why you cannot predict the outcome by eye: 3 / 10 == 0.3 is TRUE (a single correctly rounded division gives the same double as the literal 0.3) but 0.1 * 3 == 0.3 is FALSE (the product rounds a number that was already rounded). sum(rep(0.1, 3L)) == 0.3 is FALSE. The rule of the FAQ is the one to keep: equal only if computed in the same way, and not always even then.

The sequence that skips a value

seq(0, 1, by = 0.1) == 0.3 is a famous trap: it computes 0, 0.1, 0.2, ... by repeated arithmetic (from + (0:n) * by), so the fourth element is 0.30000000000000004, not 0.3, and no element is equal to 0.3. Everything that uses exact equality on it fails in the same way: which(), %in% and match(). Dividing integers ((0:10) / 10) gives correctly rounded results, so there it works, although it is luck of the operation and not a rule.

# A sequence that "skips" the value you look for
x <- seq(0, 1, by = 0.1)
x == 0.3
#>  [1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
which(x == 0.3)
#> integer(0)
which(x == 0.5)
#> [1] 6
0.3 %in% x
#> [1] FALSE
match(0.3, x)
#> [1] NA
(0:10) / 10 == 0.3
#>  [1] FALSE FALSE FALSE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
x[4L] - 0.3
#> [1] 5.551115e-17

What to do instead

Pick the approach that matches the question:

  • Compare with a tolerance. isTRUE(all.equal(x, y)) for one pair of objects, and for vectors an explicit test such as abs(x - y) < tol for each element. The near() helper in the example uses sqrt(.Machine$double.eps) as its default, the same as all.equal(); dplyr::near() is the community package’s version of the same idea. The Comparison help page makes the same recommendation: "Using all.equal with identical or isTRUE is almost always preferable".

  • Round both sides to the number of digits that matter, as in round(x, 1L) == 0.3, when the precision is part of the data (money, measurements with a fixed resolution). Rounding to decimals is still not exact in binary, but both sides get the same rounding.

  • Avoid fractions: keep money in integer cents (fee_cents == 30L) and convert to a decimal only for display. Integer arithmetic below the 2^31 limit is exact.

  • Do not use floating-point values as keys for %in%, match(), unique() or table(); use the integer they came from or a rounded string.

# What to do instead
which(abs(x - 0.3) < 1e-9)
#> [1] 4
which(vapply(x, \(v) isTRUE(all.equal(v, 0.3)), logical(1L)))
#> [1] 4
near <- function(x, y, tol = sqrt(.Machine$double.eps)) {
  abs(x - y) < tol
}
near(x, 0.3)
#>  [1] FALSE FALSE FALSE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
round(x, 1L) == 0.3
#>  [1] FALSE FALSE FALSE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
which(round(x, 1L) == 0.3)
#> [1] 4

# Late fees from the shelf scenario: 0.10 per day
fee_per_day <- 0.10
late_days <- 3L
fee <- fee_per_day * late_days
fee == 0.30
#> [1] FALSE
print(fee, digits = 17)
#> [1] 0.30000000000000004
isTRUE(all.equal(fee, 0.30))
#> [1] TRUE
round(fee, 2L) == 0.30
#> [1] TRUE
fee_cents <- 10L * late_days
fee_cents == 30L
#> [1] TRUE

The late fees of the shelf show the pattern: 0.10 * 3 is not 0.30, all.equal() and round() say it is, and integer cents never raise the question.

The order operators and the special values

The ordering operators suffer from the same rounding: sqrt(2)^2 > 2 and sqrt(2)^2 >= 2 are TRUE while sqrt(2)^2 < 2 is FALSE, because the stored result is slightly above 2. Rounding also changes integer division: 0.3 / 0.1 prints as 3 but is stored as 2.9999999999999996, so floor(0.3 / 0.1) and 0.3 %/% 0.1 are 2, the "representation error" that the Arithmetic help page warns about for %/% on non-integers. Round before flooring (floor(round(0.3 / 0.1, 9L))) or work in integers.

Inf compares as expected (Inf == Inf is TRUE), but Inf - Inf and 0 / 0 are NaN, which is not equal to anything, including itself, so use is.nan() and is.finite() to test for it (see Missing and Special Values). all.equal(NaN, NaN) is TRUE, because the two objects are the same in every respect that all.equal() looks at.

# Comparison operators have the same problem
sqrt(2)^2 > 2
#> [1] TRUE
sqrt(2)^2 >= 2
#> [1] TRUE
sqrt(2)^2 < 2
#> [1] FALSE
print(0.3 / 0.1, digits = 17)
#> [1] 2.9999999999999996
floor(0.3 / 0.1)
#> [1] 2
0.3 %/% 0.1
#> [1] 2
floor(round(0.3 / 0.1, 9L))
#> [1] 3

# Inf, NaN and the other special values
Inf == Inf
#> [1] TRUE
Inf - Inf
#> [1] NaN
Inf > .Machine$double.xmax
#> [1] TRUE
is.nan(0 / 0)
#> [1] TRUE
0 / 0 == 0 / 0
#> [1] NA
isTRUE(all.equal(NaN, NaN))
#> [1] TRUE
isTRUE(all.equal(Inf, Inf))
#> [1] TRUE

A last note from the FAQ and the Comparison help page: for the single-test case the choice is between identical() (exact, one logical, never NA) and isTRUE(all.equal()) (approximate, one logical). Never == in a condition unless you have already ruled out NA, a wrong length and rounding.

Common errors and warnings

The messages below were produced by the examples of this page, in R 4.6.1.

Message Typical cause Fix

non-numeric argument to binary operator

character, list or data frame operand of an arithmetic operator

convert (as.numeric()), or select the numeric column

invalid argument to unary operator

-"a", +"a"

same as above

longer object length is not a multiple of shorter object length

recycling of unequal vectors in arithmetic, comparison or logic (warning)

make the lengths equal or intend the recycling with a multiple

NAs produced by integer overflow

.Machine$integer.max + 1L (warning)

use doubles (drop the L)

comparison (==) is possible only for atomic and list types

comparing functions or environments with ==

use identical()

comparison (<) is not possible for language types

ordering a call or symbol

compare strings (deparse()) or use identical()

‘<’ not meaningful for factors

ordering an unordered factor (warning, result NA)

use an ordered factor or compare the labels

'list' object cannot be coerced to type 'double'

a list with an element of length other than one in a comparison

compare lengths() or unlist first

invalid argument type

!"a"

compare to a value first

operations are possible only for numeric, logical or complex types

"a" & TRUE, list(TRUE) | TRUE

convert or compare

invalid 'x' type in 'x && y'

a character or NULL operand of && or ||

test with is.null(), nzchar() or isTRUE()

'length = 2' in coercion to 'logical(1)'

a vector of length greater than one in && or || (error from R 4.3.0, warning in R 4.2.0)

reduce with any()/all(), or use & and |

the condition has length > 1

a vector in if or while (error from R 4.2.0)

reduce with any()/all(), or use ifelse()

missing value where TRUE/FALSE needed

if (NA) after a comparison with a missing value

isTRUE(), is.na(), na.rm = TRUE

argument is of length zero

if (logical(0)), often a comparison with NULL

test length() first

argument is not interpretable as logical

if (all.equal(1, 1.1)) (the result is a string)

isTRUE(all.equal(...))

non-conformable arguments

%*% with incompatible dimensions

check dim() and t()

non-conformable arrays

+ on arrays of different dimensions

make the dimensions agree

NA/NaN argument

1:NA

seq_len() after removing NA

numerical expression has 2 elements: only the first used

a vector as an endpoint of : (warning)

use one value

unexpected '<', unexpected '=='

chained comparison 1 < 2 < 3 (syntax error)

write two comparisons joined by &

could not find function "<-<-"

x <- y = 5

use x <- y <- 5

could not find function "%nope%"

an operator that was not defined or not loaded

define it or attach/import its package

Version notes

Version Change

R 4.1.0

The native pipe |> and the lambda \(x) are added; all.equal() of two functions also compares their environments.

R 4.2.0

&& and || with an argument of length greater than one give a warning; if() and while() with a condition of length greater than one are an error; the pipe placeholder _ may be used as a named argument; all.equal.numeric() checks its tolerance argument.

R 4.3.0

&& and || with an argument of length greater than one are always an error ('length = 4' in coercion to 'logical(1)'); %*% becomes an S3 generic in the group matrixOps; chooseOpsMethod() is added; _ may begin an extraction chain (experimental); all.equal.numeric() gets giveErr.

R 4.4.0

%||% is added to base R; the default and numeric methods of all.equal() get check.class.

R 4.6.0

%notin% is added to base R.

R-devel (4.7.0, not released)

The NEWS of the development version lists a new warning for x[logi] when the length of x is not a multiple of the length of the logical index; it is not part of R 4.6.1.

Where older tutorials differ

Older tutorial Current practice Why Version

if (x == y) or if (all.equal(x, y)) as the test of equality

identical(x, y), or isTRUE(all.equal(x, y)) for numbers

== returns NA and a vector, and all.equal() returns a string when the values differ (Comparison and identical help pages)

always

&& and || with a vector, "the first element is used"

reduce with any() or all() first

a warning in R 4.2.0, an error since R 4.3.0

R 4.2.0, 4.3.0

x == NA to find missing values

is.na(x)

every comparison with NA is NA

always

Own %||% in every project, or Negate() applied to %in% for "not in"

%||% (R 4.4.0) and %notin% (R 4.6.0) from base R

they are in base now; your own definition masks them and still works, but code that must run on an older R needs it

R 4.4.0, R 4.6.0

isTRUE <- function(x) identical(x, TRUE) as the definition of isTRUE

isTRUE(x) is true for a named TRUE such as c(val = TRUE) too

the help page says the old definition was false for that

current

magrittr %>% for all chains

the native pipe |>

a syntax transformation with no package and no function call of its own

R 4.1.0

function(x) x + 1 only

\(x) x + 1 is the same call

the NEWS entry says the shorthand is parsed as the long form

R 4.1.0

** for the exponent

^

** is translated by the parser and was undocumented for years; the help page notes it is deprecated in S

always

!x %in% y relied on without parentheses

!(x %in% y), or x %notin% y from R 4.6.0

the Syntax example says "we strongly advise to use the !( ... ) form"

R 4.6.0

1:n - 1, 1:length(x)

1:(n - 1), seq_len(n - 1), seq_along(x)

: binds tighter than binary minus and counts down for an empty vector

always

T and F in conditions

TRUE and FALSE

they are ordinary variables that can be reassigned

always

x == 0.3 after seq(0, 1, by = 0.1) or a sum of fractions

isTRUE(all.equal(x, 0.3)) per element, abs(x - 0.3) < tol, or integers

FAQ 7.31: only fractions with a power-of-2 denominator are exact

always

"R and S have the same operator precedence", & and | equal

& binds tighter than |, ? is the lowest

the Syntax Note lists the differences from S

always

For the full list of changes by release see What’s New and Migrating Older Code.

Where to go next

References