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 (
ReduceandMapdo 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 ( |
|
|
integer (floored) division and remainder |
|
|
ordering and comparison |
|
|
negation, and elementwise AND, OR and exclusive OR |
|
|
AND and OR on single values, evaluated left to right and only as far as needed |
|
|
membership of the left values in the right values |
|
|
the left value unless it is |
|
|
matrix product, outer product, Kronecker product |
|
|
the integer sequence from the left value to the right value |
|
|
the native pipe, the function shorthand, the formula and help operators |
|
|
assignment, covered on Assignment, Names and Values |
|
|
namespace access, component and slot extraction, indexing, covered on Subsetting and Indexing |
|
|
an operator you define yourself |
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"
Source: R documentation: Arithmetic
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
Source: R documentation: Arithmetic
Four rules in that output are worth remembering:
-
-7 %/% 2is-4, not-3: the quotient rounds down, and-7 %% 2is1, so(-4) * 2 + 1is-7. -
Dividing by zero never stops the script for doubles:
5 %/% 0isInfand5 %% 0isNaN. For integers the result isNA, because an integer has noInfand noNaN. -
%%and%/%also work on non-integer values, but the help page warns that the results are subject to representation error. Mathematically1 %/% 0.2should be 5, but the binary fraction nearest 0.2 is slightly larger than 0.2, so most platforms give 4, as the output shows, and1 %% 0.2is0.2, not0. For a doublexmuch larger thanythe remainder can lose accuracy, and R warns when it detects that. -
%/% 7and%% 7split 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
Source: R documentation: Arithmetic
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
Source: R documentation: Arithmetic
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
Source: R documentation: Arithmetic
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
Source: R documentation: Comparison
The output shows the rules that cause most comparison bugs:
-
NAandNaNare "non-comparable even to themselves", so every comparison involving them isNA.NA == NAisNA, notTRUE, andx == NAis always a vector ofNA; useis.na(). A comparison against a column with missing values keeps those positionsNA, asbooks$pages == 214Lshows, andwhich()drops them whilebooks[books$pages == 214L, ]would insert a row ofNA. -
1 == "1"isTRUEbecause the number is turned into the string"1". The same coercion explains10 < "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"
Source: R documentation: Comparison
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
Source: R documentation: Comparison
-
Comparing factors with
==and!=compares the labels, and the ordering operators giveNAwith 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 useidentical()(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, soquote(a + b) == "a + b"isTRUE, and<givescomparison (<) is not possible for language types. The help page itself saysidentical()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
Source: R documentation: Logic
The rules behind that output:
-
Numbers are coerced to logical: zero is false and every other number is true, so
!0:2isTRUE FALSE FALSE. -
NAis a valid logical value and the result isNAonly when it is ambiguous:NA & TRUEisNA, butNA & FALSEisFALSE, andNA | TRUEisTRUE. -
Raw vectors are handled bitwise by
!,&,|andxor(), and!is the ones' complement. For bit operations on integers usebitwAnd()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, soTRUE || TRUE && FALSEisTRUE.
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
Source: R documentation: Logic
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 madeif()andwhile()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"
Source: R NEWS: Changes in R 4.3.0
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"
Source: R documentation: Logic
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
Source: R documentation: Logic
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
Source: R documentation: match
%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
Source: R documentation: match
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
Source: R documentation: match
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"
Source: R documentation: Control
Three properties matter:
-
It tests only for
NULL.NA,character(0),list()andFALSEare all kept, soNA %||% "default"isNA. For "missing or empty" write the test you mean. -
The right side is evaluated lazily, like the right side of
||:"kept" %||% stop(...)never callsstop(), as the help page’s own example says ("parsed, but not evaluated"), andNULL %||% 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$nameisNULLwhen 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
Source: R documentation: matmult
%*% 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"
Source: R documentation: matmult
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
Source: R documentation: outer
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)
Source: R documentation: Colon
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"
Source: R documentation: pipeOp
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"
Source: R documentation: function
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
Source: R documentation: tilde
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
#>
Source: R documentation: Question
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% 4is2 * (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%"), andsprintf()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"
Source: R documentation: groupGeneric
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 ofOps: they cannot be overloaded, and they always requireTRUE/FALSE-like single values (see The length-1 errors of && and ||). Likewise%in%is an ordinary function that callsmatch(); to change how objects of a class are matched, provide anmtfrm()method, as thematchhelp page describes. -
The matrix product
%*%is not inOps: 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 withUseMethod(), as the last chunk does. A method named%+%.bookis found like anygeneric.classmethod.
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"
Source: R documentation: UseMethod
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 minus and plus |
prefix |
6 |
|
sequence operator |
left to right |
7 |
|
special operators, including |
left to right |
8 |
|
multiply, divide |
left to right |
9 |
|
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 |
Source: R documentation: Syntax
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 inf(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^, so1:3-1is0 1 2while1:2^3is1:8;2 ^ 2 ^ 3is2 ^ 8, not4 ^ 3, whereas1 - 1 - 1is-1, not1; 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,whileandrepeattake an expression as their body, and that body extends as far to the right as the expression can, so\(x) x + 1 * 2is a function whose body isx + 1 * 2. -
The pipe is at the level of
%any%, and the parser removes it:x |> f(y)is alreadyf(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))
Source: R documentation: Syntax
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))
Source: R documentation: Syntax
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)
Source: R documentation: Syntax
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
Source: R documentation: Syntax
-
-2^2is-4: the exponent binds tighter than the minus sign, as it does in mathematics.(-2)^2is4. -
-1:3is-1 0 1 2 3, the sequence from -1;-(1:3)negates the sequence.-5 %/% 2is-3because the minus applies to the 5 first. -
1:n - 1is(1:n) - 1, the sequence from 0 ton - 1. This is the most frequent precedence bug in R code: the sequence you meant is1:(n - 1), and for a variable lengthseq_len(n - 1). In the same way1:2^3is1:8, not(1:2)^3. -
%%and%/%bind tighter than*:10 - 2 * 7 %% 4is10 - 2 * 3.5 %/% 2 * 2is(5 %/% 2) * 2, which is4, not the1that5 %/% (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
Source: R documentation: Syntax
-
2^3^2is512: the exponent groups to the right.100 / 10 / 2is5, grouped to the left. -
!x > 3Lis!(x > 3L), so withx <- 2Lit isTRUE;(!x) > 3LisFALSE > 3L. Negation is lower than comparison and than%in%. It is higher than&and|, so!x & ynegates onlyx. -
TRUE || TRUE && FALSEisTRUE || (TRUE && FALSE), which isTRUE, while(TRUE || TRUE) && FALSEisFALSE. The same with the elementwise forms:TRUE | FALSE & FALSEisTRUE. 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
Source: R documentation: Syntax
-
Comparison operators do not chain.
1 < 2 < 3is not read as(1 < 2) < 3but rejected by the parser:unexpected '<'. The same fora == b == c. Write1 < x & x < 10, or(1 < x) & (x < 10);findInterval()andcut()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 = 5is 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 withcould not find function "<-<-". TheSyntaxhelp page describes this as a "non-catchable" error in itsNot runexample.2 + 2 <- 5parses ((2 + 2) <- 5), and fails when evaluated withtarget of assignment expands to non-language object. The valid chains arex <- y <- 5andx = 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
Source: R documentation: pipeOp
-
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()is2 * sqrt(3), which is about3.46, notsqrt(6); write(2 * 3) |> sqrt().a + b |> f()isa + 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"
Source: R documentation: identical
What identical() compares is the whole object, including its type and attributes:
-
identical(1L, 1)isFALSEwhile1L == 1isTRUE: an integer and a double are different types.identical(c(a = 1), 1)isFALSE: the names are an attribute. -
NAis notNA_integer_; with the defaultsingle.NA = TRUEthere is oneNAand oneNaNper type (soNaNis identical toNaN, butNA_real_is not identical toNaN). -
0and-0are identical by default (num.eq = TRUEcompares numbers with==);num.eq = FALSEcompares 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 = FALSEmakes it matter. -
identical(NULL, c())isTRUE, becausec()returnsNULL.
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
Source: R documentation: identical
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
Source: R documentation: all.equal
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"
Source: R documentation: all.equal
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"
Source: R documentation: stopifnot
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)isTRUE, although the numbers differ by a factor of two;tolerance = 0shows 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"
Source: R documentation: all.equal
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
Source: R documentation: .Machine
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
Source: R documentation: Comparison
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 asabs(x - y) < tolfor each element. Thenear()helper in the example usessqrt(.Machine$double.eps)as its default, the same asall.equal();dplyr::near()is the community package’s version of the same idea. TheComparisonhelp page makes the same recommendation: "Usingall.equalwithidenticalorisTRUEis 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()ortable(); 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
Source: R documentation: all.equal
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
Source: R documentation: Arithmetic
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 |
|---|---|---|
|
character, list or data frame operand of an arithmetic operator |
convert ( |
|
|
same as above |
|
recycling of unequal vectors in arithmetic, comparison or logic (warning) |
make the lengths equal or intend the recycling with a multiple |
|
|
use doubles (drop the |
|
comparing functions or environments with |
use |
|
ordering a call or symbol |
compare strings ( |
|
ordering an unordered factor (warning, result |
use an ordered factor or compare the labels |
|
a list with an element of length other than one in a comparison |
compare |
|
|
compare to a value first |
|
|
convert or compare |
|
a character or |
test with |
|
a vector of length greater than one in |
reduce with |
|
a vector in |
reduce with |
|
|
|
|
|
test |
|
|
|
|
|
check |
|
|
make the dimensions agree |
|
|
|
|
a vector as an endpoint of |
use one value |
|
chained comparison |
write two comparisons joined by |
|
|
use |
|
an operator that was not defined or not loaded |
define it or attach/import its package |
Source: R documentation: Syntax
Version notes
| Version | Change |
|---|---|
R 4.1.0 |
The native pipe |
R 4.2.0 |
|
R 4.3.0 |
|
R 4.4.0 |
|
R 4.6.0 |
|
R-devel (4.7.0, not released) |
The NEWS of the development version lists a new warning for |
Where older tutorials differ
| Older tutorial | Current practice | Why | Version |
|---|---|---|---|
|
|
|
always |
|
reduce with |
a warning in R 4.2.0, an error since R 4.3.0 |
R 4.2.0, 4.3.0 |
|
|
every comparison with |
always |
Own |
|
they are in |
R 4.4.0, R 4.6.0 |
|
|
the help page says the old definition was false for that |
current |
|
the native pipe |
a syntax transformation with no package and no function call of its own |
R 4.1.0 |
|
|
the NEWS entry says the shorthand is parsed as the long form |
R 4.1.0 |
|
|
|
always |
|
|
the |
R 4.6.0 |
|
|
|
always |
|
|
they are ordinary variables that can be reassigned |
always |
|
|
FAQ 7.31: only fractions with a power-of-2 denominator are exact |
always |
"R and S have the same operator precedence", |
|
the |
always |
For the full list of changes by release see What’s New and Migrating Older Code.
Where to go next
-
Syntax and Lexical Structure for the tokens and parse trees that this page uses.
-
Assignment, Names and Values for
<-,=,<<-and->and copy-on-modify. -
Control Flow for
ifand the conditions that the operators of this page feed. -
Numbers and Arithmetic for the numeric types, integer overflow and rounding.
-
Missing and Special Values for
NA,NaNandInfin comparisons. -
Pipes and the Native Pipe for
|>and the placeholder. -
S3 Group Generics and Operator Overloading for writing
Opsmethods properly. -
Formulas for the operators inside a formula.
-
Idioms and Common Pitfalls for the same traps in a wider list.
References
-
The R Language Definition: Operators
-
The R Language Definition: Infix and prefix operators
-
The R Language Definition: Special operators
-
The R Language Definition: Recycling rules
-
An Introduction to R: Defining new binary operators
-
R FAQ 7.31: Why doesn’t R think these numbers are equal?
-
R NEWS: Changes in R 4.1.0, 4.2.0, 4.3.0, 4.4.0 and 4.6.0, and R-devel