Tidyverse style guide
Complete, compacted version of https://style.tidyverse.org (every rule kept; prose and
examples shortened). The verbatim chapters are in references/*.md, regenerated from
upstream by scripts/sync.py. Style guides are opinionated; the point is consistency, so
fewer decisions are needed. Tools: styler restyles code (RStudio add-in); lintr
checks it.
Rules most often missed
Verified failures from agents writing R without this skill:
- Base pipe
|>, never magrittr %>% (R >= 4.3 covers everything recommended).
- 80 characters per line, counted. One-line pipes and
map(xs, \(x) ...) calls are the usual
offenders; split them one argument per line, or extract a function.
- Pipe data into
ggplot(); never filter/slice inside the data argument.
- NEWS bullets end
(@user, #issue). in that order, with the parentheses before the full stop.
While in development a bullet is one line, however long; wrapping happens at release.
- Commit subject: under 50 characters, sentence case, no period;
Fixes #<n> (not "Closes")
in the body.
return(), stop(), break, next always get their own {} block, never a one-line if.
- Anonymous functions:
\(x) x + 1, not ~ .x + 1; never \() for multi-line or named functions.
- ASCII only in
.R files (local rule, below).
Local rules
Not part of style.tidyverse.org; kept here so they survive upstream syncs. Apply them with
the same force as the guide.
- ASCII only in R source files. No character above 0x7F anywhere in a
.R file: not in
code, comments, or string literals. That rules out smart quotes, em and en dashes,
non-breaking spaces, accented letters, box-drawing characters, and emoji, including
the info and cross bullet glyphs that cli renders (name the bullets "i" and "x" in
the cli call and let cli draw them). Files cross Windows and Linux machines with
different default encodings, R CMD check warns on non-ASCII in R/, and a stray
non-breaking space is invisible yet breaks parsing. When a non-ASCII character is
genuinely needed in a string, write it as an escape: "\u00e9" for e-acute,
"\u2014" for an em dash.
scripts/check.R reports every offending line.
Part 1: Analyses
Files
Names. Machine readable: no spaces, symbols, or special characters; all lower case; never
two names differing only in case; words delimited by - or _; extension .R. Human
readable: the name describes the contents (report-draft-notes.txt, not temp.r); closely
related files share one structure (fig-eda.png, fig-model-3.png). Sort correctly by
default: dates as yyyy-mm-dd (ISO 8601); numbers zero-padded so 11 does not sort before 2;
if order matters, number at the start, not the end (01-load-data.R,
02-exploratory-analysis.R). If you missed a step, rename all files rather than adding
02a, 02b. Never use "final" or similar in a name; rely on Git, or failing that, date the
file (report-2022-03-20.qmd, not FinalReport-2.qmd).
# Good # Bad
fit_models.R fit models.R
exploratory-data-analysis.R ExploratoryDataAnalysis.r
2025-01-01-report.Rmd jan 01 report.Rmd
Organisation. If a file can be given a concise name that still evokes its contents, the
organisation is good. Getting there is hard.
Internal structure. Break a file into chunks with commented lines of - and =. Load all
add-on packages together at the very top of the file; do not sprinkle library() calls
through the script or hide dependencies in .Rprofile.
# Load data ---------------------------
# Plot data ---------------------------
Syntax
Object names
Only lowercase letters, numbers, and _; separate words with _ (snake case): day_one,
day_1, not DayOne, dayone. Reserve . for the S3 system (methods are
function.class; dots elsewhere give things like as.data.frame.data.frame()). If you are
cramming data into names (model_2018, model_2019), use a list or data frame instead.
Variables are nouns, functions are verbs. Concise and meaningful: day_one, not
first_day_of_the_month or djm1. Do not reuse names of common functions or variables:
never T <- FALSE, c <- 10, mean <- function(x) sum(x).
Spacing
- Commas: space after, never before:
x[, 1], not x[,1], x[ ,1], x[ , 1].
- Parentheses: no spaces inside or outside for function calls:
mean(x, na.rm = TRUE).
Space before and after () with if, for, while: if (debug) {. Space after () in
a function definition: function(x) {}, not function (x) {} or function(x){}.
- Embracing
{{ }} always has inner spaces: group_by({{ by }}), not {{by}}.
- Infix operators (
==, +, -, <-, = in arguments, etc.) always surrounded by
spaces: height <- (feet * 12) + inches. Exceptions, never spaced:
- high-precedence operators
::, :::, $, @, [, [[, ^, unary -, unary +,
: (sqrt(x^2 + y^2), df$z, 1:10);
- single-sided formulas whose right-hand side is a single identifier (
~foo;
tribble(~col1, ~col2, ...)), but a complex right-hand side does take a space:
~ .x + .y;
!! and !!! in tidy evaluation: call(!!xyz);
- the help operator:
?mean, package?stats.
- Extra spaces are fine to align
= or <- (mean = (a + b + c) / n under
total = a + b + c); never add space where it is not usually allowed.
Vertical space
Sparingly, to separate "thoughts" like paragraph breaks. No empty lines at the start or end
of a function; a single empty line only where needed to separate functions or pipes; an
empty line before a comment block often helps tie the comment to its code.
Function calls
Named arguments. Arguments are either data or details. Omit names of data
arguments; name every detail argument whose default you override:
mean(1:10, na.rm = TRUE), not mean(x = 1:10, , FALSE). Never partial-match:
rep(1:2, times = 3), not rep(1:2, t = 3).
Assignment in calls. Avoid: x <- f(); if (nzchar(x) < 1), not
if (nzchar(x <- f()) < 1). Only exception: functions that capture side effects,
output <- capture.output(x <- f()).
Long calls. Limit lines to 80 characters; running out of room means extract a
function or use early returns to reduce nesting. When a call does not fit, one line each
for the function name, every argument, and the closing ):
do_something_very_complicated(
something = "that",
requires = many,
arguments = "some of which may be long"
)
Unnamed common arguments may stay unnamed, but if that makes line lengths very
uneven, name them anyway (x = x, y = long_argument_name, ...). Closely related unnamed
arguments may share a line, typically so one line of code matches one line of output:
paste0(
"Requirement: ", requires, "\n",
"Result: ", result, "\n"
)
Braced expressions
{} defines the main hierarchy of R code (function bodies, control flow, calls such as
tryCatch() and test_that()). { is the last character on its line, with the related
code (the if clause, function declaration, trailing comma) on that same line; contents
indented two spaces; } first character on its line; else on the same line as }.
if (y < 0 && debug) {
message("y is negative")
}
test_that("call1 returns an ordered factor", {
expect_s3_class(call1(x, y), c("factor", "ordered"))
})
tryCatch(
{
x <- scan()
cat("Total: ", sum(x), "\n", sep = "")
},
interrupt = function(e) {
message("Aborted by user")
}
)
An empty braced expression is written {} with no space or blank line inside:
function(...) {}.
Control flow
Loops (for, while, repeat): the body must be braced, even one statement.
A waiting while loop may have an empty {} body.
If statements. A single-line if never contains braces and is only for very simple
expressions with no side effects and no control-flow change:
message <- if (x > 10) "big" else "small". Bad: if (x > 10) { "big" } else { "small" };
if (x > 0) message <- "big" else message <- "small"; if (x > 0) return(x). A multi-line
if must use braces (an unbraced if ... else over lines only parses inside {} or a
call). Avoid implicit coercion in the condition: if (length(x) > 0), not
if (length(x)). Never & or | in an if condition (they return vectors); always &&
and ||. ifelse(x, a, b) is not a drop-in for if (x) a else b: it is vectorised
(recycles a, b to length(x)) and eager (evaluates both).
Control flow modifiers (return(), stop(), break, next) always in their own
{} block:
if (y < 0) {
stop("Y is negative")
}
for (x in xs) {
if (is_done(x)) {
break
}
}
Switch. Prefer names to positions (never switch(y, 1, 2, 3)). Each element on its
own line unless all fit on one. Fall-through elements have a space after = (a = ,).
Provide a fall-through error unless input was validated earlier:
switch(x,
a = ,
b = 1,
c = 2,
stop("Unknown `x`", call. = FALSE)
)
Semicolons, assignment, data, comments
- Semicolons: never; not at line ends, not to join commands on one line.
- Assignment:
<-, not =: x <- 5.
- Strings:
" not '. Only exception: text containing double quotes and no single
quotes, 'Text with "quotes"'. Never 'Text with "double" and 'single' quotes'.
- Logicals:
TRUE/FALSE, not T/F.
- Comments: every line starts
# (symbol plus one space). In analysis code, record
findings and decisions. If comments are needed to explain what the code does, rewrite the
code; if there are more comments than code, switch to R Markdown/Quarto.
Functions
Naming: verbs. add_row(), permute(); not row_adder(), permutation().
Anonymous functions: \(x) x + 1 for short lambdas defined inline in an argument.
map(xs, \(x) mean((x + 5)^2)) or function(x) ...; not map(xs, ~ mean((.x + 5)^2)).
Never \() for multi-line functions (use function(x) {) or for named functions
(cv <- function(x) {, not cv <- \(x) sd(x) / mean(x)). Avoid \() inside a pipe. Use
informative argument names.
Multi-line definitions. Each argument on its own line, in one of two forms.
Single-indent: arguments indented two spaces, ) and { together on a new line.
Hanging-indent: arguments aligned with the opening (, ) { on the last argument's
line. Never indent the continuation arguments only two spaces under a hanging first
argument (hides where the definition ends). An argument that will not fit on one line
should be reworked to be short and sweet.
# Single-indent
long_function_name <- function(
a = "a long argument",
b = "another argument"
) {
# body indented two spaces as usual
}
# Hanging-indent
long_function_name <- function(a = "a long argument",
b = "another argument") {
# body
}
# Bad: definition and body blur together
long_function_name <- function(a = "a long argument",
b = "another argument") {
# body
}
S7 methods: the name is a method() call, so use single-indent. If the method call
itself is too long, spread its arguments over lines with the usual rules, then
) <- function( with single-indent arguments.
return(): only for early returns; otherwise rely on the last expression
(add_two <- function(x, y) { x + y }, not return(x + y)). A return() always gets its
own braced line (see control flow modifiers). Functions called for side effects (print,
plot, save) return the first argument invisible(x) so they can be piped; print
methods should do this:
print.url <- function(x, ...) {
cat("Url: ", build_url(x), "\n", sep = "")
invisible(x)
}
Comments in code explain why, not what or how (# Objects like data frames are treated as leaves, not # Recurse only with bare lists). Start each line # . Sentence
case. End with a full stop only if the comment has two or more sentences.
Pipes
Use |> to emphasise a sequence of actions on one object. Works with any function via the
_ placeholder: strings |> gsub("a", "b", x = _).
Do not pipe when manipulating more than one object at a time, or when there are
meaningful intermediate objects worth naming.
Whitespace: space before |>, then usually a newline; after the first step, each line
indented two spaces. Never hang a pipe with unindented continuation lines.
Long lines: when a step's arguments do not fit, one argument per line, indented. In
data analysis, use the pipe whenever a call spans multiple lines, even for a single step
(iris |> summarise(...), not summarise(iris, ...)).
Short pipes may sit on one line (iris |> subset(Species == "virginica") |> _$Sepal.Length), but since short pipes grow, prefer one function per line. A short inline
pipe as an argument is acceptable when it reads better than a lookup
(x |> semi_join(y |> filter(is_valid))); otherwise pull it out and name it:
x_join <- x |> select(a, b, w)
y_join <- y |> select(a, b, v)
left_join(x_join, y_join, join_by(a, b))
Assignment, three acceptable forms: name and <- on their own line above the pipe;
name <- object |> on the first line; or -> at the end (... |> arrange(-value) -> iris_long). The -> form is natural to write but harder to read; a leading name acts as a
heading.
magrittr: use base |>, not %>%. As of R 4.3.0 the base pipe has every magrittr
feature that is recommended.
ggplot2
+ between layers follows the same rules as |>: space before +, newline after (even
with two layers), each subsequent line indented two spaces. When the plot follows a dplyr
pipeline, keep a single level of indentation (do not indent the geoms further). When a
layer's arguments do not fit on one line, one argument per line, indented. Do data
manipulation (filter, slice) in the pipeline before plotting, never inside the data
argument.
# Good
iris |>
filter(Species == "setosa") |>
ggplot(aes(x = Sepal.Width, y = Sepal.Length)) +
geom_point() +
labs(
x = "Sepal width, in cm",
y = "Sepal length, in cm"
)
# Bad
ggplot(filter(iris, Species == "setosa"), aes(x = Sepal.Width, y = Sepal.Length)) +
geom_point()
Part 2: Packages
Package files
Everything in Files above applies; in addition: a file with one function takes that
function's name; a file with several related functions takes a concise, evocative name;
deprecated functions live in a file prefixed deprec-. Public functions and their
documentation come first, private helpers after all documented functions. Several public
functions sharing one documentation block all immediately follow it:
#' Lots of functions for doing something cool
#'
#' ... Complete documentation ...
#' @name something-cool
NULL
#' @describeIn something-cool Get the mean
#' @export
get_cool_mean <- function(x) {
# ...
}
Documentation (roxygen2)
Use roxygen2 with markdown enabled; documentation matters even if the only user is
future-you.
- Title and description. First line is a concise title (function, dataset, or class),
sentence case, no trailing full stop. No explicit
@title/@description tags, except
@description when the description has multiple paragraphs or formatting such as a
bulleted list.
- Indents and line breaks. One space after
#'. Continuation lines of a tag's text get
two extra spaces (#' as column headings.). Alternatively, multi-line tags
(@description, @examples, @section) may start on their own line with unindented
continuation. Blank #' lines between sections where needed (@section Tidy data: ends
with a colon).
- Parameters.
@param, @seealso, @return text is a sentence: capital letter, full
stop. Shared parameters: @inheritParams function_to_inherit_from.
- Capitalisation and full stops. Every bullet, enumeration item, and argument description
is sentence case and ends with a full stop, even if only a few words. Do not capitalise
function or package names (R is case sensitive). A colon precedes an enumeration or list.
- Cross-linking. Encouraged, internal and external. Closely related functions go in
@seealso: a single one as a sentence ([fct_lump()] to automatically ...), several as
a bulleted list. Link other packages fully qualified: [pkg::function()]. Related
families: @family single table verbs (family names plural); this auto-generates
@seealso lists. External links: bare URL in <> or prose that makes the destination
obvious; never "click here".
- R code in backticks: argument names (
na.rm), values (TRUE, NA, NULL, ...),
literal code, class names (tbl_df). Function names may be `tibble()` but consider
the link [tibble()] instead; link only the first mention per topic.
- Package names: no code font. If ambiguous as an ordinary word, follow with "package"
or wrap in
{}, not both: "Use the glue package" or "Use {glue}", never "glue" or
"the {glue} package". At the start of a sentence, keep lower case: "dplyr provides ...".
- Internal functions: document with
#' as usual and add @noRd so no .Rd is
generated.
Tests
Test files mirror R/ files: a function in R/foofy.R is tested in
tests/testthat/test-foofy.R. Create with usethis::use_test(). The file name appears in
test output, giving context.
Error messages
Assumes cli::cli_abort() (bulleted lists, glue interpolation, inline markup, error
chaining, control of the reported call); the advice applies to stop() with more work.
Structure: general problem statement, then bulleted details, then an optional
hint.
- Problem statement. Concise, informative, sentence case, ends with a full stop. If the
cause is clear (wrong type or size) use must, stating both what was expected and what
was received:
`n` must be a numeric vector, not a character vector.;
`n` must have length 1, not length 2. If you cannot state what was expected use
can't: Can't find column `b` in `.data`.; Can't coerce `.x` to a vector.
- Location. Ideally name the failing function call (
Error in validate_mapping():);
see rlang's error-call topic for passing calls through helpers.
- Details. Bulleted list after the statement: cross bullets (
x) say what is wrong,
info bullets (i) give context. Short sentences: ! Can't subset elements past the end. /
i Location 100 doesn't exist. / i There are only 26 elements. Reveal the location,
name, or content of the offending input (x Result 1 is a character vector.); this may
mean passing extra arguments so low-level errors know the original source, which is worth
it for frequently used functions. If the source is unclear, do not guess which argument
is at fault: Can't find column `b` in `.data`., not `.data` must contain column `b`.;
Tibble columns must have compatible sizes. with per-size bullets, not blaming column x.
Multiple issues or inconsistencies across arguments: one bullet each
(x `.x` has length 4 / x `.y` has length 2), not one sentence. Truncate long
lists (NAs found at 1,000,000 locations: 1, 2, 3, ...). Pluralise with ngettext()
(see ?ngettext for translation caveats).
- Hints. Only when the cause is clear and common (check patterns of misuse, e.g. on
StackOverflow). Last bullet, info bullet, ends with a question mark:
i Did you mean `Species == "setosa"`?; i Did you use `%>%` or `|>` instead of `+`?
Most valuable when the error surfaces far from its root cause (Can't subset a function. /
i Have you forgotten to define a variable named `mean`?). Bad hints steer users
wrong, so be conservative.
- Punctuation. Sentence case, full stop; bullets likewise, first word capitalised
unless it is an argument or column name. Prefer the singular in problem statements
(
Each result must be coercible to a single integer., not Results must be ...). When
several problems are detectable, list up to five, then ... and 5 more problems.
Connector between problem and location: ", not", ";", or ":" as fits. Argument names in
backticks; say Column `x` to distinguish columns from arguments; avoid "variable"
(ambiguous). Each component under 80 characters; no manual line breaks (wrong at other
console widths); break into bullets instead, or let cli wrap paragraphs.
- Before and after (real tidyverse rewrites):
Argument 2 filter condition does not evaluate to a logical vector. became Each argument must be a logical vector. /
* Argument 2 (`cyl`) is an integer vector.; geom_line requires the following missing aesthetics: y became `geom_line()` must have the following aesthetics: `y`.;
`xxx` contains unknown variables and Evaluation error: object 'xxx' not found.
both became Can't find column `xxx` in `.data`.; Expected at least one column name; e.g. name`` became Must supply at least one column name, e.g. name.
- Localisation. Be informative but keep every sentence simple so messages can be
translated later, even if you do not localise now.
NEWS.md
Every user-facing change gets a bullet; minor documentation changes do not, but sweeping
changes and new vignettes do. Write for users, not developers: what changed and why it
matters to them; purely internal changes get no bullet.
- In development. New bullets go at the top, directly under the first heading
(
# pkg (development version)), as a single line; wrapping and grouping happen at
release. Include the issue number; for a PR by a non-author, include the GitHub user name.
Both in parentheses, before the final period:
* `ggsave()` now uses full argument names to avoid partial match warnings (@wch, #2355).
- Pre-release. Proofread, groom, organise. Function name as close to the start as
possible (
`ggsave()` now ..., not Fixed ... in ggsave()). Wrap to 80 characters; every bullet ends with a full stop. Positive framing and present tense: what now happens (`now uses full argument names`), not what used to (`no longer partially matches`). One sentence suffices for fixes and minor improvements; new features may need more, and complex ones a fenced code example (useful later for the blog post): * In stat_bin(), binwidth now also takes functions. The function is called with the scaled x values, and should return a single number. ... ``
- Code style. Functions, arguments, file names in backticks; functions with
(); omit
"the argument"/"the function": In `stat_bin()`, `binwidth` now also takes functions.
- Headings. Level 1 per release:
# modelr 0.1.2. Small releases need nothing more.
Many bullets: level 2 groups, commonly ## Breaking changes, ## New features,
## Minor improvements and fixes; deviate or subdivide (level 3) when it helps, as
ggplot2 2.3.0 did (## New features > ### Tidy evaluation, ### sf, ...). Do not group
during development. Within a section, order bullets alphabetically by the first function
mentioned; bullets naming no function go at the top.
- Breaking changes. Own section at the top; each bullet describes the symptoms and the
fix (e.g. condition on
packageVersion("tidyr") > "0.7.2"), and is repeated in its
topical section.
- Common patterns. New family: describe the behaviour and cite (
@karawoo, #1526). New
function: * New `stat_qq_line()` makes it easy to .... New argument:
* `geom_segment()` gains a `linejoin` parameter. Argument behaviour change:
* In `separate()`, `col = -1` now refers to ... Previously, and incorrectly, ...
Function behaviour change:
* `map()` and `modify()` now work with calls and pairlists (#412).
- Blog post. Every major and minor release: highlight major user-facing changes with
examples, point to the release notes for details, skip minor fixes.
Part 3: Git and GitHub
- Commit messages follow standard advice (chris.beams.io/posts/git-commit): subject
line under 50 characters, sentence case, no trailing period; blank line, then explanation
and context in paragraphs if needed;
Fixes #<issue-number> when it closes an issue so
merging to main closes it.
- Pull requests. Title briefly describes the change, stands alone, and does not include
the issue number (no
Fixes #10 in the title). Simple change: description may be blank.
Complex change: overview of the changes, plus Fixes #<issue-number> in the description.
Checking code mechanically
After writing or restyling .R files, run the bundled checker if R is available. It applies
lintr's tidyverse defaults plus the guide's non-default rules (base pipe only, library()
at the top, no ~ .x lambdas, &&/|| in conditions, no assignment inside calls,
implicit returns), the local ASCII-only rule, and a styler dry run for layout.
Rscript scripts/check.R path/to/file.R # report; exit 1 if anything is off
Rscript scripts/check.R --fix path/to/file.R # let styler fix layout first, then report
Fix what it reports, then re-run until it exits 0. It cannot judge names, comments, roxygen
wording, error-message wording, NEWS, or commits; review those against the sections above.
One known gap: styler puts every switch() element on its own line, while the guide allows
one line when all elements fit, so that particular styler diff may be ignored.
Staying in sync with upstream
references/ holds the verbatim chapters and references/UPSTREAM.json the commit they came
from; metadata.upstream_commit above is the commit this file was compacted from.
python scripts/sync.py # fetch upstream, regenerate references/, report drift
python scripts/sync.py --check # exit 1 if references/ or this file are behind upstream
python scripts/sync.py --mark # after folding upstream changes into this file
When the report shows changed chapters, read the printed diff, update the matching section
here (keep every rule, shorten prose), then run --mark.
1---2name: tidyverse-style3description: Apply the tidyverse style guide whenever the deliverable is R code, or text inside an R project, that must follow tidyverse conventions: writing new R functions, scripts, or packages; restyling or cleaning up existing .R files; reviewing R pull requests for style; and answering questions about how R code should be laid out (naming, spacing, indentation, line breaks, pipes, ggplot2 chains, braces, return(), comments). Also covers roxygen2 documentation, testthat file layout, cli error-message wording, NEWS.md bullets, and commit or PR messages for R packages. Do not use when the user only wants existing R code explained or walked through, a runtime error diagnosed, wrong results or empty joins debugged, or R tooling installed, and not for non-R languages or ordinary prose.4---56# Tidyverse style guide78Complete, compacted version of <https://style.tidyverse.org> (every rule kept; prose and9examples shortened). The verbatim chapters are in `references/*.md`, regenerated from10upstream by `scripts/sync.py`. Style guides are opinionated; the point is consistency, so11fewer decisions are needed. Tools: **styler** restyles code (RStudio add-in); **lintr**12checks it.1314## Rules most often missed1516Verified failures from agents writing R without this skill:1718- Base pipe `|>`, never magrittr `%>%` (R >= 4.3 covers everything recommended).19- 80 characters per line, counted. One-line pipes and `map(xs, \(x) ...)` calls are the usual20 offenders; split them one argument per line, or extract a function.21- Pipe data into `ggplot()`; never filter/slice inside the `data` argument.22- NEWS bullets end `(@user, #issue).` in that order, with the parentheses before the full stop.23 While in development a bullet is one line, however long; wrapping happens at release.24- Commit subject: under 50 characters, sentence case, no period; `Fixes #<n>` (not "Closes")25 in the body.26- `return()`, `stop()`, `break`, `next` always get their own `{}` block, never a one-line `if`.27- Anonymous functions: `\(x) x + 1`, not `~ .x + 1`; never `\()` for multi-line or named functions.28- ASCII only in `.R` files (local rule, below).2930## Local rules3132Not part of style.tidyverse.org; kept here so they survive upstream syncs. Apply them with33the same force as the guide.3435- **ASCII only in R source files.** No character above 0x7F anywhere in a `.R` file: not in36 code, comments, or string literals. That rules out smart quotes, em and en dashes,37 non-breaking spaces, accented letters, box-drawing characters, and emoji, including38 the info and cross bullet glyphs that cli renders (name the bullets `"i"` and `"x"` in39 the cli call and let cli draw them). Files cross Windows and Linux machines with40 different default encodings, `R CMD check` warns on non-ASCII in `R/`, and a stray41 non-breaking space is invisible yet breaks parsing. When a non-ASCII character is42 genuinely needed in a string, write it as an escape: `"\u00e9"` for e-acute,43 `"\u2014"` for an em dash.44 `scripts/check.R` reports every offending line.4546---4748# Part 1: Analyses4950## Files5152**Names.** Machine readable: no spaces, symbols, or special characters; all lower case; never53two names differing only in case; words delimited by `-` or `_`; extension `.R`. Human54readable: the name describes the contents (`report-draft-notes.txt`, not `temp.r`); closely55related files share one structure (`fig-eda.png`, `fig-model-3.png`). Sort correctly by56default: dates as `yyyy-mm-dd` (ISO 8601); numbers zero-padded so 11 does not sort before 2;57if order matters, number at the start, not the end (`01-load-data.R`,58`02-exploratory-analysis.R`). If you missed a step, rename all files rather than adding59`02a`, `02b`. Never use "final" or similar in a name; rely on Git, or failing that, date the60file (`report-2022-03-20.qmd`, not `FinalReport-2.qmd`).6162```63# Good # Bad64fit_models.R fit models.R65exploratory-data-analysis.R ExploratoryDataAnalysis.r662025-01-01-report.Rmd jan 01 report.Rmd67```6869**Organisation.** If a file can be given a concise name that still evokes its contents, the70organisation is good. Getting there is hard.7172**Internal structure.** Break a file into chunks with commented lines of `-` and `=`. Load all73add-on packages together at the very top of the file; do not sprinkle `library()` calls74through the script or hide dependencies in `.Rprofile`.7576```r77# Load data ---------------------------7879# Plot data ---------------------------80```8182## Syntax8384### Object names8586Only lowercase letters, numbers, and `_`; separate words with `_` (snake case): `day_one`,87`day_1`, not `DayOne`, `dayone`. Reserve `.` for the S3 system (methods are88`function.class`; dots elsewhere give things like `as.data.frame.data.frame()`). If you are89cramming data into names (`model_2018`, `model_2019`), use a list or data frame instead.90Variables are nouns, functions are verbs. Concise and meaningful: `day_one`, not91`first_day_of_the_month` or `djm1`. Do not reuse names of common functions or variables:92never `T <- FALSE`, `c <- 10`, `mean <- function(x) sum(x)`.9394### Spacing9596- **Commas:** space after, never before: `x[, 1]`, not `x[,1]`, `x[ ,1]`, `x[ , 1]`.97- **Parentheses:** no spaces inside or outside for function calls: `mean(x, na.rm = TRUE)`.98 Space before and after `()` with `if`, `for`, `while`: `if (debug) {`. Space after `()` in99 a function definition: `function(x) {}`, not `function (x) {}` or `function(x){}`.100- **Embracing** `{{ }}` always has inner spaces: `group_by({{ by }})`, not `{{by}}`.101- **Infix operators** (`==`, `+`, `-`, `<-`, `=` in arguments, etc.) always surrounded by102 spaces: `height <- (feet * 12) + inches`. Exceptions, never spaced:103 - high-precedence operators `::`, `:::`, `$`, `@`, `[`, `[[`, `^`, unary `-`, unary `+`,104 `:` (`sqrt(x^2 + y^2)`, `df$z`, `1:10`);105 - single-sided formulas whose right-hand side is a single identifier (`~foo`;106 `tribble(~col1, ~col2, ...)`), but a complex right-hand side does take a space:107 `~ .x + .y`;108 - `!!` and `!!!` in tidy evaluation: `call(!!xyz)`;109 - the help operator: `?mean`, `package?stats`.110- **Extra spaces** are fine to align `=` or `<-` (`mean = (a + b + c) / n` under111 `total = a + b + c`); never add space where it is not usually allowed.112113### Vertical space114115Sparingly, to separate "thoughts" like paragraph breaks. No empty lines at the start or end116of a function; a single empty line only where needed to separate functions or pipes; an117empty line before a comment block often helps tie the comment to its code.118119### Function calls120121- **Named arguments.** Arguments are either *data* or *details*. Omit names of data122 arguments; name every detail argument whose default you override:123 `mean(1:10, na.rm = TRUE)`, not `mean(x = 1:10, , FALSE)`. Never partial-match:124 `rep(1:2, times = 3)`, not `rep(1:2, t = 3)`.125- **Assignment in calls.** Avoid: `x <- f(); if (nzchar(x) < 1)`, not126 `if (nzchar(x <- f()) < 1)`. Only exception: functions that capture side effects,127 `output <- capture.output(x <- f())`.128- **Long calls.** Limit lines to 80 characters; running out of room means extract a129 function or use early returns to reduce nesting. When a call does not fit, one line each130 for the function name, every argument, and the closing `)`:131132 ```r133 do_something_very_complicated(134 something = "that",135 requires = many,136 arguments = "some of which may be long"137 )138 ```139140 Unnamed common arguments may stay unnamed, but if that makes line lengths very141 uneven, name them anyway (`x = x, y = long_argument_name, ...`). Closely related unnamed142 arguments may share a line, typically so one line of code matches one line of output:143144 ```r145 paste0(146 "Requirement: ", requires, "\n",147 "Result: ", result, "\n"148 )149 ```150151### Braced expressions152153`{}` defines the main hierarchy of R code (function bodies, control flow, calls such as154`tryCatch()` and `test_that()`). `{` is the last character on its line, with the related155code (the `if` clause, function declaration, trailing comma) on that same line; contents156indented two spaces; `}` first character on its line; `else` on the same line as `}`.157158```r159if (y < 0 && debug) {160 message("y is negative")161}162163test_that("call1 returns an ordered factor", {164 expect_s3_class(call1(x, y), c("factor", "ordered"))165})166167tryCatch(168 {169 x <- scan()170 cat("Total: ", sum(x), "\n", sep = "")171 },172 interrupt = function(e) {173 message("Aborted by user")174 }175)176```177178An empty braced expression is written `{}` with no space or blank line inside:179`function(...) {}`.180181### Control flow182183- **Loops** (`for`, `while`, `repeat`): the body must be braced, even one statement.184 A waiting `while` loop may have an empty `{}` body.185- **If statements.** A single-line `if` never contains braces and is only for very simple186 expressions with no side effects and no control-flow change:187 `message <- if (x > 10) "big" else "small"`. Bad: `if (x > 10) { "big" } else { "small" }`;188 `if (x > 0) message <- "big" else message <- "small"`; `if (x > 0) return(x)`. A multi-line189 `if` must use braces (an unbraced `if ... else` over lines only parses inside `{}` or a190 call). Avoid implicit coercion in the condition: `if (length(x) > 0)`, not191 `if (length(x))`. Never `&` or `|` in an `if` condition (they return vectors); always `&&`192 and `||`. `ifelse(x, a, b)` is not a drop-in for `if (x) a else b`: it is vectorised193 (recycles `a`, `b` to `length(x)`) and eager (evaluates both).194- **Control flow modifiers** (`return()`, `stop()`, `break`, `next`) always in their own195 `{}` block:196197 ```r198 if (y < 0) {199 stop("Y is negative")200 }201 for (x in xs) {202 if (is_done(x)) {203 break204 }205 }206 ```207208- **Switch.** Prefer names to positions (never `switch(y, 1, 2, 3)`). Each element on its209 own line unless all fit on one. Fall-through elements have a space after `=` (`a = ,`).210 Provide a fall-through error unless input was validated earlier:211212 ```r213 switch(x,214 a = ,215 b = 1,216 c = 2,217 stop("Unknown `x`", call. = FALSE)218 )219 ```220221### Semicolons, assignment, data, comments222223- **Semicolons:** never; not at line ends, not to join commands on one line.224- **Assignment:** `<-`, not `=`: `x <- 5`.225- **Strings:** `"` not `'`. Only exception: text containing double quotes and no single226 quotes, `'Text with "quotes"'`. Never `'Text with "double" and 'single' quotes'`.227- **Logicals:** `TRUE`/`FALSE`, not `T`/`F`.228- **Comments:** every line starts `# ` (symbol plus one space). In analysis code, record229 findings and decisions. If comments are needed to explain *what* the code does, rewrite the230 code; if there are more comments than code, switch to R Markdown/Quarto.231232## Functions233234- **Naming:** verbs. `add_row()`, `permute()`; not `row_adder()`, `permutation()`.235- **Anonymous functions:** `\(x) x + 1` for short lambdas defined inline in an argument.236 `map(xs, \(x) mean((x + 5)^2))` or `function(x) ...`; not `map(xs, ~ mean((.x + 5)^2))`.237 Never `\()` for multi-line functions (use `function(x) {`) or for named functions238 (`cv <- function(x) {`, not `cv <- \(x) sd(x) / mean(x)`). Avoid `\()` inside a pipe. Use239 informative argument names.240- **Multi-line definitions.** Each argument on its own line, in one of two forms.241 *Single-indent:* arguments indented two spaces, `)` and `{` together on a new line.242 *Hanging-indent:* arguments aligned with the opening `(`, `) {` on the last argument's243 line. Never indent the continuation arguments only two spaces under a hanging first244 argument (hides where the definition ends). An argument that will not fit on one line245 should be reworked to be short and sweet.246247 ```r248 # Single-indent249 long_function_name <- function(250 a = "a long argument",251 b = "another argument"252 ) {253 # body indented two spaces as usual254 }255256 # Hanging-indent257 long_function_name <- function(a = "a long argument",258 b = "another argument") {259 # body260 }261262 # Bad: definition and body blur together263 long_function_name <- function(a = "a long argument",264 b = "another argument") {265 # body266 }267 ```268269- **S7 methods:** the name is a `method()` call, so use single-indent. If the method call270 itself is too long, spread its arguments over lines with the usual rules, then271 `) <- function(` with single-indent arguments.272- **`return()`:** only for early returns; otherwise rely on the last expression273 (`add_two <- function(x, y) { x + y }`, not `return(x + y)`). A `return()` always gets its274 own braced line (see control flow modifiers). Functions called for side effects (print,275 plot, save) return the first argument `invisible(x)` so they can be piped; `print`276 methods should do this:277278 ```r279 print.url <- function(x, ...) {280 cat("Url: ", build_url(x), "\n", sep = "")281 invisible(x)282 }283 ```284285- **Comments** in code explain *why*, not what or how (`# Objects like data frames are286 treated as leaves`, not `# Recurse only with bare lists`). Start each line `# `. Sentence287 case. End with a full stop only if the comment has two or more sentences.288289## Pipes290291- Use `|>` to emphasise a sequence of actions on one object. Works with any function via the292 `_` placeholder: `strings |> gsub("a", "b", x = _)`.293- **Do not pipe** when manipulating more than one object at a time, or when there are294 meaningful intermediate objects worth naming.295- **Whitespace:** space before `|>`, then usually a newline; after the first step, each line296 indented two spaces. Never hang a pipe with unindented continuation lines.297- **Long lines:** when a step's arguments do not fit, one argument per line, indented. In298 data analysis, use the pipe whenever a call spans multiple lines, even for a single step299 (`iris |> summarise(...)`, not `summarise(iris, ...)`).300- **Short pipes** may sit on one line (`iris |> subset(Species == "virginica") |>301 _$Sepal.Length`), but since short pipes grow, prefer one function per line. A short inline302 pipe as an argument is acceptable when it reads better than a lookup303 (`x |> semi_join(y |> filter(is_valid))`); otherwise pull it out and name it:304305 ```r306 x_join <- x |> select(a, b, w)307 y_join <- y |> select(a, b, v)308 left_join(x_join, y_join, join_by(a, b))309 ```310311- **Assignment**, three acceptable forms: name and `<-` on their own line above the pipe;312 `name <- object |>` on the first line; or `->` at the end (`... |> arrange(-value) ->313 iris_long`). The `->` form is natural to write but harder to read; a leading name acts as a314 heading.315- **magrittr:** use base `|>`, not `%>%`. As of R 4.3.0 the base pipe has every magrittr316 feature that is recommended.317318## ggplot2319320`+` between layers follows the same rules as `|>`: space before `+`, newline after (even321with two layers), each subsequent line indented two spaces. When the plot follows a dplyr322pipeline, keep a single level of indentation (do not indent the geoms further). When a323layer's arguments do not fit on one line, one argument per line, indented. Do data324manipulation (filter, slice) in the pipeline before plotting, never inside the `data`325argument.326327```r328# Good329iris |>330 filter(Species == "setosa") |>331 ggplot(aes(x = Sepal.Width, y = Sepal.Length)) +332 geom_point() +333 labs(334 x = "Sepal width, in cm",335 y = "Sepal length, in cm"336 )337338# Bad339ggplot(filter(iris, Species == "setosa"), aes(x = Sepal.Width, y = Sepal.Length)) +340 geom_point()341```342343---344345# Part 2: Packages346347## Package files348349Everything in *Files* above applies; in addition: a file with one function takes that350function's name; a file with several related functions takes a concise, evocative name;351deprecated functions live in a file prefixed `deprec-`. Public functions and their352documentation come first, private helpers after all documented functions. Several public353functions sharing one documentation block all immediately follow it:354355```r356#' Lots of functions for doing something cool357#'358#' ... Complete documentation ...359#' @name something-cool360NULL361362#' @describeIn something-cool Get the mean363#' @export364get_cool_mean <- function(x) {365 # ...366}367```368369## Documentation (roxygen2)370371Use roxygen2 with markdown enabled; documentation matters even if the only user is372future-you.373374- **Title and description.** First line is a concise title (function, dataset, or class),375 sentence case, no trailing full stop. No explicit `@title`/`@description` tags, except376 `@description` when the description has multiple paragraphs or formatting such as a377 bulleted list.378- **Indents and line breaks.** One space after `#'`. Continuation lines of a tag's text get379 two extra spaces (`#' as column headings.`). Alternatively, multi-line tags380 (`@description`, `@examples`, `@section`) may start on their own line with unindented381 continuation. Blank `#'` lines between sections where needed (`@section Tidy data:` ends382 with a colon).383- **Parameters.** `@param`, `@seealso`, `@return` text is a sentence: capital letter, full384 stop. Shared parameters: `@inheritParams function_to_inherit_from`.385- **Capitalisation and full stops.** Every bullet, enumeration item, and argument description386 is sentence case and ends with a full stop, even if only a few words. Do not capitalise387 function or package names (R is case sensitive). A colon precedes an enumeration or list.388- **Cross-linking.** Encouraged, internal and external. Closely related functions go in389 `@seealso`: a single one as a sentence (`[fct_lump()] to automatically ...`), several as390 a bulleted list. Link other packages fully qualified: `[pkg::function()]`. Related391 families: `@family single table verbs` (family names plural); this auto-generates392 `@seealso` lists. External links: bare URL in `<>` or prose that makes the destination393 obvious; never "click here".394- **R code** in backticks: argument names (`na.rm`), values (`TRUE`, `NA`, `NULL`, `...`),395 literal code, class names (`tbl_df`). Function names may be `` `tibble()` `` but consider396 the link `[tibble()]` instead; link only the first mention per topic.397- **Package names:** no code font. If ambiguous as an ordinary word, follow with "package"398 or wrap in `{}`, not both: "Use the glue package" or "Use {glue}", never "`glue`" or399 "the {glue} package". At the start of a sentence, keep lower case: "dplyr provides ...".400- **Internal functions:** document with `#'` as usual and add `@noRd` so no `.Rd` is401 generated.402403## Tests404405Test files mirror `R/` files: a function in `R/foofy.R` is tested in406`tests/testthat/test-foofy.R`. Create with `usethis::use_test()`. The file name appears in407test output, giving context.408409## Error messages410411Assumes `cli::cli_abort()` (bulleted lists, glue interpolation, inline markup, error412chaining, control of the reported call); the advice applies to `stop()` with more work.413Structure: general **problem statement**, then bulleted **details**, then an optional414**hint**.415416- **Problem statement.** Concise, informative, sentence case, ends with a full stop. If the417 cause is clear (wrong type or size) use **must**, stating both what was expected and what418 was received: `` `n` must be a numeric vector, not a character vector. ``;419 `` `n` must have length 1, not length 2. `` If you cannot state what was expected use420 **can't**: `` Can't find column `b` in `.data`. ``; `` Can't coerce `.x` to a vector. ``421- **Location.** Ideally name the failing function call (`Error in `validate_mapping()`:`);422 see rlang's error-call topic for passing calls through helpers.423- **Details.** Bulleted list after the statement: cross bullets (`x`) say what is wrong,424 info bullets (`i`) give context. Short sentences: `! Can't subset elements past the end.` /425 `i Location 100 doesn't exist.` / `i There are only 26 elements.` Reveal the location,426 name, or content of the offending input (`x Result 1 is a character vector.`); this may427 mean passing extra arguments so low-level errors know the original source, which is worth428 it for frequently used functions. If the source is unclear, do not guess which argument429 is at fault: `` Can't find column `b` in `.data`. ``, not `` `.data` must contain column `b`. ``;430 `Tibble columns must have compatible sizes.` with per-size bullets, not blaming column `x`.431 Multiple issues or inconsistencies across arguments: one bullet each432 (`` x `.x` has length 4 `` / `` x `.y` has length 2 ``), not one sentence. Truncate long433 lists (`NAs found at 1,000,000 locations: 1, 2, 3, ...`). Pluralise with `ngettext()`434 (see `?ngettext` for translation caveats).435- **Hints.** Only when the cause is clear and common (check patterns of misuse, e.g. on436 StackOverflow). Last bullet, info bullet, ends with a question mark:437 `` i Did you mean `Species == "setosa"`? ``; `` i Did you use `%>%` or `|>` instead of `+`? ``438 Most valuable when the error surfaces far from its root cause (`Can't subset a function.` /439 `` i Have you forgotten to define a variable named `mean`? ``). Bad hints steer users440 wrong, so be conservative.441- **Punctuation.** Sentence case, full stop; bullets likewise, first word capitalised442 unless it is an argument or column name. Prefer the singular in problem statements443 (`Each result must be coercible to a single integer.`, not `Results must be ...`). When444 several problems are detectable, list up to five, then `... and 5 more problems`.445 Connector between problem and location: ", not", ";", or ":" as fits. Argument names in446 backticks; say `` Column `x` `` to distinguish columns from arguments; avoid "variable"447 (ambiguous). Each component under 80 characters; no manual line breaks (wrong at other448 console widths); break into bullets instead, or let cli wrap paragraphs.449- **Before and after** (real tidyverse rewrites): `Argument 2 filter condition does not450 evaluate to a logical vector.` became `Each argument must be a logical vector.` /451 `` * Argument 2 (`cyl`) is an integer vector. ``; `geom_line requires the following452 missing aesthetics: y` became `` `geom_line()` must have the following aesthetics: `y`. ``;453 `` `xxx` contains unknown variables `` and `Evaluation error: object 'xxx' not found.`454 both became `` Can't find column `xxx` in `.data`. ``; `Expected at least one column name;455 e.g. `~name`` became `Must supply at least one column name, e.g. `~name`.`456- **Localisation.** Be informative but keep every sentence simple so messages can be457 translated later, even if you do not localise now.458459## NEWS.md460461Every user-facing change gets a bullet; minor documentation changes do not, but sweeping462changes and new vignettes do. Write for users, not developers: what changed and why it463matters to them; purely internal changes get no bullet.464465- **In development.** New bullets go at the top, directly under the first heading466 (`# pkg (development version)`), as a single line; wrapping and grouping happen at467 release. Include the issue number; for a PR by a non-author, include the GitHub user name.468 Both in parentheses, before the final period:469 `` * `ggsave()` now uses full argument names to avoid partial match warnings (@wch, #2355). ``470- **Pre-release.** Proofread, groom, organise. Function name as close to the start as471 possible (`` `ggsave()` now ... ``, not `Fixed ... in `ggsave()``). Wrap to 80 characters;472 every bullet ends with a full stop. Positive framing and present tense: what now happens473 (`now uses full argument names`), not what used to (`no longer partially matches`). One474 sentence suffices for fixes and minor improvements; new features may need more, and475 complex ones a fenced code example (useful later for the blog post):476 `` * In `stat_bin()`, `binwidth` now also takes functions. The function is called with ``477 `` the scaled `x` values, and should return a single number. ... ``478- **Code style.** Functions, arguments, file names in backticks; functions with `()`; omit479 "the argument"/"the function": `` In `stat_bin()`, `binwidth` now also takes functions. ``480- **Headings.** Level 1 per release: `# modelr 0.1.2`. Small releases need nothing more.481 Many bullets: level 2 groups, commonly `## Breaking changes`, `## New features`,482 `## Minor improvements and fixes`; deviate or subdivide (level 3) when it helps, as483 ggplot2 2.3.0 did (`## New features` > `### Tidy evaluation`, `### sf`, ...). Do not group484 during development. Within a section, order bullets alphabetically by the first function485 mentioned; bullets naming no function go at the top.486- **Breaking changes.** Own section at the top; each bullet describes the symptoms and the487 fix (e.g. condition on `packageVersion("tidyr") > "0.7.2"`), and is repeated in its488 topical section.489- **Common patterns.** New family: describe the behaviour and cite (`@karawoo, #1526`). New490 function: `` * New `stat_qq_line()` makes it easy to ... ``. New argument:491 `` * `geom_segment()` gains a `linejoin` parameter. `` Argument behaviour change:492 `` * In `separate()`, `col = -1` now refers to ... Previously, and incorrectly, ... ``493 Function behaviour change:494 `` * `map()` and `modify()` now work with calls and pairlists (#412). ``495- **Blog post.** Every major and minor release: highlight major user-facing changes with496 examples, point to the release notes for details, skip minor fixes.497498---499500# Part 3: Git and GitHub501502- **Commit messages** follow standard advice (chris.beams.io/posts/git-commit): subject503 line under 50 characters, sentence case, no trailing period; blank line, then explanation504 and context in paragraphs if needed; `Fixes #<issue-number>` when it closes an issue so505 merging to main closes it.506- **Pull requests.** Title briefly describes the change, stands alone, and does not include507 the issue number (no `Fixes #10` in the title). Simple change: description may be blank.508 Complex change: overview of the changes, plus `Fixes #<issue-number>` in the description.509510---511512## Checking code mechanically513514After writing or restyling `.R` files, run the bundled checker if R is available. It applies515lintr's tidyverse defaults plus the guide's non-default rules (base pipe only, `library()`516at the top, no `~ .x` lambdas, `&&`/`||` in conditions, no assignment inside calls,517implicit returns), the local ASCII-only rule, and a styler dry run for layout.518519```bash520Rscript scripts/check.R path/to/file.R # report; exit 1 if anything is off521Rscript scripts/check.R --fix path/to/file.R # let styler fix layout first, then report522```523524Fix what it reports, then re-run until it exits 0. It cannot judge names, comments, roxygen525wording, error-message wording, NEWS, or commits; review those against the sections above.526One known gap: styler puts every `switch()` element on its own line, while the guide allows527one line when all elements fit, so that particular styler diff may be ignored.528529## Staying in sync with upstream530531`references/` holds the verbatim chapters and `references/UPSTREAM.json` the commit they came532from; `metadata.upstream_commit` above is the commit this file was compacted from.533534```bash535python scripts/sync.py # fetch upstream, regenerate references/, report drift536python scripts/sync.py --check # exit 1 if references/ or this file are behind upstream537python scripts/sync.py --mark # after folding upstream changes into this file538```539540When the report shows changed chapters, read the printed diff, update the matching section541here (keep every rule, shorten prose), then run `--mark`.