7 R Code Style
Follow these code style guidelines for all R code:
7.1 General Principles
Our lab follows the Google R Style Guide, which in turn mostly follows the tidyverse style guide (Wickham 2023). The following principles apply to all R code:
Follow tidyverse conventions: Our style is built on the tidyverse style guide, which provides comprehensive guidance on naming, syntax, pipes, functions, and more
Naming: Use
snake_casefor functions and variables; acronyms may be uppercase (e.g.,prep_IDs_data)Write tidy code: Keep code clean, readable, and well-organized
Avoid redundant logical comparisons: Use logical variables directly in conditional statements (e.g.,
if (x)instead ofif (x == TRUE)orif (x == 1))Use pipes to emphasize primary inputs: When writing functions and code, use the pipe operator to clearly show transformations on a primary object. The primary input should flow as the first argument to each function in the chain. Design functions so the most important argument (usually data) comes first, enabling natural pipeline composition. See the tidyverse design principles for more details.
Use native pipe:
|>not%>%(available in R >= 4.1.0). This is enforced by our.lintr.Rconfiguration viapipe_consistency_linter(pipe = "|>")
Many of these rules are automatically enforced through our .lintr.R configuration file. See Section 7.17 for details on automated style checking.
7.2 Function Structure and Documentation
Every function should follow this pattern:
#' Short Title (One Line)
#'
#' Longer description providing details about what the function does,
#' when to use it, and important considerations.
#'
#' @param param1 Description of first parameter, including type and constraints
#' @param param2 Description of second parameter
#'
#' @returns Description of return value, including type and structure
#'
#' @examples
#' # Example usage
#' result <- my_function(param1 = "value", param2 = 10)
#'
#' @export
my_function <- function(param1, param2) {
# Implementation
return(result)
}See also Section 6.14 for general code documentation practices.
7.2.1 Explicit Return Statements
Following the Google R Style Guide’s recommendation, always use explicit return() statements in functions, even when R’s implicit return would work. This makes the function’s intent clear and improves readability.
Example:
# Good: Explicit return
calculate_mean <- function(x) {
result <- mean(x, na.rm = TRUE)
return(result)
}
# Less clear: Implicit return (avoid)
calculate_mean <- function(x) {
mean(x, na.rm = TRUE)
}Note: Our .lintr.R configuration disables the return_linter to allow flexibility in code, but we still require explicit returns as a lab standard.
7.3 Avoiding Deep Nesting
When writing code, avoid nested function calls and nested function definitions where feasible:
- Prefer named intermediate variables (or a pipe, e.g.
|>/%>%in R) over deeply nested calls likef(g(h(x))). Naming each step makes the data flow read top-to-bottom and leaves intermediate values inspectable in a debugger. - Prefer standalone, top-level function definitions over functions defined inside other functions. Nested definitions hide reusable logic, complicate unit testing, and obscure scope.
This is a readability/maintainability default, not an absolute rule — keep the nesting when flattening it would be more convoluted (a trivial one-argument wrapper, or a closure that genuinely needs the enclosing scope).
7.4 Lambdas in map()/apply-family calls
The nested-definition rule applies to purrr::map*() / pmap*() / lapply()-family call sites too: don’t wrap a named function in an anonymous function (lambda) just to fix constant arguments. Pass the mapped elements positionally and the constants through the mapping function’s ...:
# Preferred --- hr/power match schoenfeld_events()'s leading parameters
# positionally; the constant fractions ride along via map2's `...`
purrr::map2_dbl(
df$hr, df$power, schoenfeld_events,
p1 = frac_op, p2 = frac_nonop
)
# Avoid --- a lambda that only fixes constant arguments
purrr::map2_dbl(df$hr, df$power, function(h, p) {
schoenfeld_events(hr = h, power = p, p1 = frac_op, p2 = frac_nonop)
})purrr’s own documentation (since 1.0; see the “Extra arguments” note in ?map) mildly recommends the opposite — shorthand lambdas over ...-passing. This preference deliberately overrides that: when the mapped elements line up with the callee’s leading parameters, use ... and skip the wrapper. Don’t relitigate this in review rounds; cite this fragment instead.
When a wrapper genuinely is necessary — the mapped element isn’t the callee’s leading argument, is used more than once in the body, or the body is a real expression rather than a single call — define a named wrapper function in its own file (see one-function-per-file) rather than a lambda. The one exception is a demonstrated performance reason to define the wrapper nested inside the calling function (e.g. it must close over a large enclosing-scope object that would otherwise be passed repeatedly); that is the same closure escape hatch as the nested-definition rule above.
Apply this when writing code and when reviewing it: a map-site lambda that only fixes constants is a review finding, the same weight as the other nesting findings. (Encoded from review feedback on ucdavis/rampp#137, where two power-table builders wrapped schoenfeld_events() and total_n_for_power_unequal() in lambdas that ...-passing replaced.)
7.5 Prefer Existing Packaged Functions
Before writing a function, look for an existing packaged one that already does the job — and prefer it over rolling your own:
- Check, roughly in this order: base R and the tidyverse / r-lib packages, then a focused, well-maintained CRAN package, then our own lab packages (e.g.
{bcs},{ettbc},{gha}, and the shared workflows there). Packages can depend on each other, so reuse across our repos is fine. - Reach for the packaged version unless it is genuinely unfit — the wrong API, a heavy dependency for a one-liner, or it doesn’t quite do what you need.
Packaged functions are tested, documented, and maintained by other people; hand-rolling an equivalent duplicates that work, adds surface area to maintain, and risks subtle bugs the package already fixed. For example, use withr::with_seed() to set a seed and restore the RNG stream, rather than hand-rolling a .Random.seed save/restore.
This is a default, not an absolute rule. A tiny, dependency-free helper can beat pulling in a package, and sometimes nothing fits — but look first, and prefer the standard, well-known way over a bespoke one.
This is the R-function special case of the broader don’t-reinvent-the-wheel principle — see dont-reinvent-wheel for the general statement, which also covers whole features, the fork-or-contribute preference for close-but-not-exact matches, and the review-side application.
7.6 Prefer Per-Operation Grouping
When reviewing or writing dplyr code, prefer per-operation grouping (the .by argument) over persistent group_by() / ungroup() pairs. Apply it when the grouping is only needed for one operation.
# Preferred — grouping is scoped to this summarise(), no ungroup() needed
df |> summarise(mean_x = mean(x), .by = group_col)
# Avoid — group_by() persists and must be manually ungroup()'d
df |> group_by(group_col) |> summarise(mean_x = mean(x)) |> ungroup()Reference: https://dplyr.tidyverse.org/reference/dplyr_by.html
Flag persistent group_by() calls during code review when .by would work — that is, when the grouping feeds exactly one downstream verb and no subsequent operation needs it to persist.
7.7 Avoid Hard-Coding Data with an External Source of Truth
Avoid hard-coding data that already has a reliable external source of truth — a version number, a package list, a dependency’s release date, a set of downstream consumers, a schema, an enum’s valid values. Read or generate it from that source instead of copying a snapshot into the codebase:
- Versions and pins. Don’t retype a dependency’s version in prose or a second config file when a lockfile,
DESCRIPTION, or manifest already states it — reference that file, or generate the mention from it. - Generated lists. A list of consumers, plugins, or registered items that the source system can enumerate (an API, a directory scan, a registry) should be produced by querying that system, not maintained by hand alongside it.
- Cross-file duplication. When the same fact must appear in two places (a usage example and a reference doc, a schema and its example), generate the second from the first, or have CI check they agree, rather than trusting two hand-edited copies to stay in sync.
This is conditioned on the external source being reliably available — don’t add a network fetch or a fragile dependency where a static value would do. A constant that has no external owner (a magic number intrinsic to the algorithm, a default chosen by this project) is not “hard-coded data” in this sense — it is just a value. The target is duplicated ownership of a fact: if updating the external source should have updated this value too, and didn’t, that is the bug this guidance prevents.
7.8 Construct Complex Inputs Before the Call
Build a complex argument as a named intermediate first, then pass that name to the function. Naming the intermediate keeps the call short, makes the data flow read top to bottom, and lets you inspect the value in a debugger.
# Good: name the intermediate, then pass it
model_vars <- c("age", "sex", "titer")
model_formula <- reformulate(model_vars, response = "outcome")
fit <- lm(model_formula, data = study_data)
# Avoid: complex input constructed inline in the call
fit <- lm(reformulate(c("age", "sex", "titer"), response = "outcome"), data = study_data)A pipe is the other idiomatic way to avoid an inline-constructed argument, when the input is the result of a short sequence of transformations:
# Good: build the input with a pipe, then pass it
recent_cases <-
case_data |>
filter(year >= 2017) |>
arrange(onset_date)
ggplot(recent_cases, aes(x = onset_date)) +
geom_histogram()This is the same readability goal as Section 7.3: prefer named steps you can read and check over one dense expression.
7.10 Line Breaks and Formatting
7.10.1 Blank Lines Before Lists
Always include a blank line before starting a bullet list or numbered list in markdown/Quarto documents. This ensures proper rendering and readability.
Correct:
Here are the requirements:
- First item
- Second itemIncorrect:
Here are the requirements:
- First item
- Second itemHere’s what happens if you don’t add the blank line:
Here are the requirements: - First item - Second item
7.10.2 Semantic Line Breaks in Plain Text
Add a newline at the end of every phrase or logical unit of text in plain-text source files. A phrase is typically a complete thought, clause, or sentence. This applies to:
- Plain-text paragraphs in
.qmdfiles - Source code text: comments, documentation strings, and error messages
Correct (prose in .qmd):
When talking about code in prose sections,
use backticks to apply code formatting.
This helps maintain readability in source files
and makes diffs easier to review.Incorrect (prose in .qmd):
When talking about code in prose sections, use backticks to apply code formatting. This helps maintain readability in source files and makes diffs easier to review.Correct (R code comment):
# First, check if the input is valid.
# Then, process the data.
# Finally, return the result.Incorrect (R code comment):
# First, check if the input is valid. Then, process the data. Finally, return the result.This practice is also known as semantic line breaks.
Guidelines:
- Break after complete sentences (at periods)
- Break after long phrases or clauses (at commas or conjunctions)
- Aim for lines under 80 characters
- Keep related short phrases together on one line
- Do not break in the middle of inline code, links, or formatting
7.10.3 Line Breaks in Code
- For
ggplotcalls anddplyrpipelines, do not crowd single lines. Here are some nontrivial examples of “beautiful” pipelines, where beauty is defined by coherence:
# Example 1
school_names = list(
OUSD_school_names = absentee_all |>
filter(dist.n == 1) |>
pull(school) |>
unique |>
sort,
WCCSD_school_names = absentee_all |>
filter(dist.n == 0) |>
pull(school) |>
unique |>
sort
)# Example 2
absentee_all = fread(file = raw_data_path) |>
mutate(program = case_when(schoolyr %in% pre_program_schoolyrs ~ 0,
schoolyr %in% program_schoolyrs ~ 1)) |>
mutate(period = case_when(schoolyr %in% pre_program_schoolyrs ~ 0,
schoolyr %in% LAIV_schoolyrs ~ 1,
schoolyr %in% IIV_schoolyrs ~ 2)) |>
filter(schoolyr != "2017-18")And of a complex ggplot call:
# Example 3
ggplot(data=data) +
aes(x=.data[["year"]], y=.data[["rd"]], group=.data[[group]]) +
geom_point(mapping = aes(col = .data[[group]], shape = .data[[group]]),
position=position_dodge(width=0.2),
size=2.5) +
geom_errorbar(mapping = aes(ymin=.data[["lb"]], ymax= .data[["ub"]], col= .data[[group]]),
position=position_dodge(width=0.2),
width=0.2) +
geom_point(position=position_dodge(width=0.2),
size=2.5) +
geom_errorbar(mapping=aes(ymin=lb, ymax=ub),
position=position_dodge(width=0.2),
width=0.1) +
scale_y_continuous(limits=limits,
breaks=breaks,
labels=breaks) +
scale_color_manual(std_legend_title,values=cols,labels=legend_label) +
scale_shape_manual(std_legend_title,values=shapes, labels=legend_label) +
geom_hline(yintercept=0, linetype="dashed") +
xlab("Program year") +
ylab(yaxis_lab) +
theme_complete_bw() +
theme(strip.text.x = element_text(size = 14),
axis.text.x = element_text(size = 12)) +
ggtitle(title)Imagine (or perhaps mournfully recall) the mess that can occur when you don’t strictly style a complicated ggplot call. Trying to fix bugs and ensure your code is working can be a nightmare. Now imagine trying to do it with the same code 6 months after you’ve written it. Invest the time now and reap the rewards as the code practically explains itself, line by line.
7.11 Markdown and Quarto Formatting
7.11.1 Writing about code in Quarto documents
When writing about code in prose sections of quarto documents, use backticks to apply a code style: for example, dplyr::mutate(). When talking about packages, use backticks and curly-braces with a hyperlink to the package website. For example: {dplyr}.
Important: Do not use raw HTML (<a href="...">) in .qmd files. Always use Quarto/markdown link syntax instead.
7.12 Messaging and User Communication
Use {cli} package functions for all user-facing messages in package functions. This is enforced by our .lintr.R configuration via undesirable_function_linter().
Required messaging functions:
- Use
cli::cli_inform()instead ofmessage(),inform(), or oldercli_alert_*()functions - Use
cli::cli_warn()instead ofwarning()orwarn() - Use
cli::cli_abort()instead ofstop()orabort()
This provides better formatting, color support, and consistent messaging across our packages.
Examples:
# Good
cli::cli_inform("Analysis complete")
cli::cli_warn("Missing data detected")
cli::cli_abort("Invalid input: {.arg x} must be numeric")
# Bad - don't use these in package code
message("Analysis complete")
warning("Missing data detected")
stop("Invalid input")7.13 Package Code Practices
- No
library()in package code: Use::notation or declare in DESCRIPTION Imports. This is enforced by our.lintr.Rconfiguration viaundesirable_function_linter(). Instead, use:::for explicit namespace references (e.g.,dplyr::mutate())usethis::use_import_from()to declare imports inNAMESPACEwithr::local_package()for temporary package loading in tests
- This keeps the global search path clean and makes dependencies explicit. See R Packages - The R landscape for more details.
- Document all exports: Use roxygen2 (
@title,@description,@param,@returns,@examples) - Avoid code duplication: Extract repeated logic into helper functions
7.14 Tidyverse Replacements
Use modern tidyverse/alternatives for base R functions:
# Data structures
tibble::tibble() # instead of data.frame()
tibble::tribble() # instead of manual data.frame creation
# I/O
readr::read_csv() # instead of read.csv()
readr::write_csv() # instead of write.csv()
readr::read_rds() # instead of readRDS()
readr::write_rds() # instead of saveRDS()
# Data manipulation
dplyr::bind_rows() # instead of rbind()
dplyr::bind_cols() # instead of cbind()
# String operations
stringr::str_which() # instead of grep()
stringr::str_replace() # instead of gsub()
# Date/time operations
lubridate::NA_Date_ # instead of as.Date(NA)
# Session info
sessioninfo::session_info() # instead of sessionInfo()See also Section 6.37.
7.15 The here Package
The here package helps manage file paths in projects by automatically finding the project root and building paths relative to it:
library(here)
# Automatically finds project root and builds paths
data <- readr::read_csv(here("data-raw", "survey.csv"))
saveRDS(results, here("inst", "analyses", "results.rds"))This solves the problem of different working directory paths across collaborators. For example, one person might have the project at /home/oski/Some-R-Project while another has it at /home/bear/R-Code/Some-R-Project. The here package handles this automatically.
This works regardless of where collaborators clone the repository. For more details, see the here package vignette.
See also Section 6.26 for detailed explanation of the here package.
7.16 Object Naming
Use descriptive names that are both expressive and explicit. Being verbose is useful and easy in the age of autocompletion:
# Good
vaccination_coverage_2017_18
absentee_flu_residuals
# Less good
vaxcov_1718
flu_resPrefer nouns for objects and verbs for functions:
# Good
clean_data <- prep_study_data(raw_data) # verb for function, noun for object
# Less clear
data <- process(input)Generally we recommend using nouns for objects and verbs for functions. This is because functions are performing actions, while objects are not.
Use consistent prefixes to signal what a function returns:
add_...()for functions that add one or more columns to adata.frameortibbleand return the modified table.compute_...()for functions that compute and return a single vector.
# Good
add_age_group <- function(data) {
data |> dplyr::mutate(age_group = cut(age, breaks = c(0, 18, 65, Inf)))
}
compute_age_group <- function(age) {
cut(age, breaks = c(0, 18, 65, Inf))
}
# Less clear
make_age_group <- function(data) { ... }
get_age_group <- function(age) { ... }This distinction makes the return type visible from the call site without reading the function body.
Use snake_case for all variable and function names. Avoid using . in names (as in base R’s read.csv()), as this goes against best practices in modern R and other languages. Modern packages like readr::read_csv() follow this convention.
Uppercase acronyms are allowed in snake_case names (e.g., prep_IDs_data, calculate_BMI_score). This is enforced via a custom object_name_linter regex pattern in our .lintr.R configuration.
Try to make your variable names both more expressive and more explicit. Being a bit more verbose is useful and easy in the age of autocompletion! For example, instead of naming a variable vaxcov_1718, try naming it vaccination_coverage_2017_18. Similarly, flu_res could be named absentee_flu_residuals, making your code more readable and explicit.
Base R allows . in variable names and functions (such as read.csv()), but this goes against best practices for variable naming in many other coding languages. For consistency’s sake, snake_case has been adopted across languages, and modern packages and functions typically use it (i.e. readr::read_csv()). As a very general rule of thumb, if a package you’re using doesn’t use snake_case, there may be an updated version or more modern package that does, bringing with it the variety of performance improvements and bug fixes inherent in more mature and modern software.
You may also see camelCase throughout the R code you come across. This is okay but not ideal – try to stay consistent across all your code with snake_case.
Again, it’s also worth noting there’s nothing inherently wrong with using . in variable names, just that it goes against style best practices that are cropping up in data science, so it’s worth getting rid of these bad habits now.
For more help, check out Be Expressive: How to Give Your Variables Better Names
7.17 Automated Tools for Style and Project Workflow
7.17.1 Styling
7.17.1.1 RStudio shortcuts
Code Autoformatting - RStudio includes a fantastic built-in utility (keyboard shortcut:
CMD-Shift-A(Mac) orCtrl-Shift-A(Windows/Linux)) for autoformatting highlighted chunks of code to fit many of the best practices listed here. It generally makes code more readable and fixes a lot of the small things you may not feel like fixing yourself. Try it out as a “first pass” on some code of yours that doesn’t follow many of these best practices!Assignment Aligner - A cool R package allows you to very powerfully format large chunks of assignment code to be much cleaner and much more readable. Follow the linked instructions and create a keyboard shortcut of your choosing (recommendation:
CMD-Shift-Z). Here is an example of how assignment aligning can dramatically improve code readability:
# Before
OUSD_not_found_aliases = list(
"Brookfield Village Elementary" = str_subset(string = OUSD_school_shapes$schnam, pattern = "Brookfield"),
"Carl Munck Elementary" = str_subset(string = OUSD_school_shapes$schnam, pattern = "Munck"),
"Community United Elementary School" = str_subset(string = OUSD_school_shapes$schnam, pattern = "Community United"),
"East Oakland PRIDE Elementary" = str_subset(string = OUSD_school_shapes$schnam, pattern = "East Oakland Pride"),
"EnCompass Academy" = str_subset(string = OUSD_school_shapes$schnam, pattern = "EnCompass"),
"Global Family School" = str_subset(string = OUSD_school_shapes$schnam, pattern = "Global"),
"International Community School" = str_subset(string = OUSD_school_shapes$schnam, pattern = "International Community"),
"Madison Park Lower Campus" = "Madison Park Academy TK-5",
"Manzanita Community School" = str_subset(string = OUSD_school_shapes$schnam, pattern = "Manzanita Community"),
"Martin Luther King Jr Elementary" = str_subset(string = OUSD_school_shapes$schnam, pattern = "King"),
"PLACE @ Prescott" = "Preparatory Literary Academy of Cultural Excellence",
"RISE Community School" = str_subset(string = OUSD_school_shapes$schnam, pattern = "Rise Community")
)# After
OUSD_not_found_aliases = list(
"Brookfield Village Elementary" = str_subset(string = OUSD_school_shapes$schnam, pattern = "Brookfield"),
"Carl Munck Elementary" = str_subset(string = OUSD_school_shapes$schnam, pattern = "Munck"),
"Community United Elementary School" = str_subset(string = OUSD_school_shapes$schnam, pattern = "Community United"),
"East Oakland PRIDE Elementary" = str_subset(string = OUSD_school_shapes$schnam, pattern = "East Oakland Pride"),
"EnCompass Academy" = str_subset(string = OUSD_school_shapes$schnam, pattern = "EnCompass"),
"Global Family School" = str_subset(string = OUSD_school_shapes$schnam, pattern = "Global"),
"International Community School" = str_subset(string = OUSD_school_shapes$schnam, pattern = "International Community"),
"Madison Park Lower Campus" = "Madison Park Academy TK-5",
"Manzanita Community School" = str_subset(string = OUSD_school_shapes$schnam, pattern = "Manzanita Community"),
"Martin Luther King Jr Elementary" = str_subset(string = OUSD_school_shapes$schnam, pattern = "King"),
"PLACE @ Prescott" = "Preparatory Literary Academy of Cultural Excellence",
"RISE Community School" = str_subset(string = OUSD_school_shapes$schnam, pattern = "Rise Community")
)7.17.1.2 {styler}
{styler} is another cool R package from the Tidyverse that can be powerful and used as a first pass on entire projects that need refactoring. The most useful function of the package is the style_dir function, which will style all files within a given directory. See the function’s documentation and the vignette linked above for more details.
The default Tidyverse styler is subtly different from some of the things we’ve advocated for in this document. Most notably we differ with regards to the assignment operator (<- vs =) and number of spaces before/after “tokens” (i.e. Assignment Aligner add spaces before = signs to align them properly). For this reason, we’d recommend the following: style_dir(path = ..., scope = "line_breaks", strict = FALSE). You can also customize {styler} even more if you’re really hardcore.
As is mentioned in the package vignette linked above, {styler} modifies things in-place, meaning it overwrites your existing code and replaces it with the updated, properly styled code. This makes it a good fit on projects with version control, but if you don’t have backups or a good way to revert back to the initial code, I wouldn’t recommend going this route.
For automated styling of entire projects:
# Install styler
install.packages("styler")
# Style all files in R/ directory
styler::style_dir("R/")
# Style entire package
styler::style_pkg()
# Note: styler modifies files in-place
# Always use with version control so you can review changes7.17.1.3 {lintr}
Linters are programming tools that check adherence to a given style, syntax errors, and possible semantic issues. The R linter, called {lintr}, helps keep files consistent across different authors and even different organizations. For example, it notifies you if you have unused variables, global variables with no visible binding, not enough or superfluous whitespace, and improper use of parentheses or brackets. A list of its other purposes can be found in this link, and most guidelines are based on the Tidyverse R Style Guide.
You can customize your settings to set defaults or to exclude files. More details can be found here.
The lintr package goes hand in hand with the styler package. The styler can be used to automatically fix the problems that the lintr catches.
7.17.2 Using Lintr
lintr package
For checking code style without modifying files:
# Install lintr (and pkgload, used below)
install.packages(c("lintr", "pkgload"))
# For package code, load the package first (see note below)
pkgload::load_all()
# Lint the entire package
lintr::lint_package()
# Lint a specific file
lintr::lint("R/my_function.R")The linter checks for:
- Unused variables
- Improper whitespace
- Line length issues
- Style guide violations
For package code, run pkgload::load_all() (or devtools::load_all()) before lintr::lint_package(). Our configuration includes object_usage_linter(), which resolves symbols using the loaded package; without loading first, it checks against a stale installed copy (or none), producing spurious no visible binding for global variable warnings. Loading also runs any .onLoad() side effects: for example, our lms linter package registers its rex shortcuts in .onLoad(), and the linter only sees them once the package is loaded.
Our lab uses .lintr.R files for configuration (the .lintr format is also supported by lintr, but we prefer .lintr.R for better R syntax support).
7.17.3 Our Lab’s Lintr Configuration
Our lab uses a custom .lintr.R configuration file in each repository to enforce our style standards. You can view the lab-manual’s configuration at https://github.com/UCD-SERG/lab-manual/blob/main/.lintr.R.
Key linters we enable:
pipe_consistency_linter(pipe = "|>"): Enforces use of native pipe|>instead of%>%object_name_linter(): Enforcessnake_casewith custom regex allowing uppercase acronymsundesirable_function_linter(): Prohibits base messaging functions andlibrary()in package coderedundant_equals_linter(): Catches redundant= TRUEwhenTRUEis the default
Linters we disable:
return_linter(return_style = "explicit"): Every function should end withreturn(return_value)rather than justreturn_value. We may sometimes disable this linter in older projects when we aren’t ready to clean this issue up, but for all new code, we require explicit returns as a lab standard, following the Google R Style Guide.trailing_whitespace_linter: Disabled (handled bystylerinstead)
Exceptions:
Our configuration allows relaxed rules for certain directories:
data-raw/: Pipe consistency and undesirable function rules relaxed (exploratory scripts)vignettes/: Undesirable function and object naming rules relaxed (tutorial code may needlibrary())inst/examples/: Undesirable function rules relaxedtests/testthat.R: Undesirable function rules relaxed
7.17.4 Linting Changed Files vs. the Whole Project in CI
A project can fall out of lint compliance without any change to its own source code or .lintr configuration: {lintr} releases sometimes add default linters or tighten existing ones, so code that passed yesterday can fail after a routine package update.
When a continuous-integration job lints the whole project (for example, lintr::lint_dir() on every pull request), those newly introduced findings surface on whatever pull request happens to run CI next. That pull request then has to expand into unrelated files just to turn the lint check green, which conflicts with our expectation that a pull request stays scoped to one concern.
In most cases, a better default is to lint changed files only, so pre-existing findings elsewhere in the project don’t block unrelated work. r-lib/actions provides a ready-made lint-changed-files example workflow; install it with usethis::use_github_action("lint-changed-files").
Whole-project linting still has a place: run it on a schedule or as a manually triggered workflow, so that project-wide drift is still detected and cleaned up deliberately in its own dedicated pull request, rather than as a side effect of someone else’s change.
7.18 Additional Resources
- Tidyverse style guide (Wickham 2023): Detailed coding style conventions for writing clear, consistent R code. Covers naming, syntax, pipes, functions, and more.
7.9 Comments
Use comments to explain why, not what:
File headers (for scripts in
data-raw/orinst/analyses/):File Structure - Just as your data “flows” through your project, data should flow naturally through a script. Very generally, you want to
Each of these sections should be “chunked together” using comments. See this file for a good example of how to cleanly organize a file in a way that follows this “flow” and functionally separate pieces of code that are doing different things.
If your computer isn’t able to handle this workflow due to RAM or requirements, modifying the ordering of your code to accommodate it won’t be ultimately helpful and your code will be fragile, not to mention less readable and messy. You need to look into high-performance computing (HPC) resources in this case.
Single-Line Comments - Commenting your code is an important part of reproducibility and helps document your code for the future. When things change or break, you’ll be thankful for comments. There’s no need to comment excessively or unnecessarily, but a comment describing what a large or complex chunk of code does is always helpful. See this file for an example of how to comment your code and notice that comments are always in the form of:
Multi-Line Comments - Occasionally, multi-line comments are necessary. You should manually insert line breaks to “hard-wrap” code and comments, whenever lines become longer than 80 characters.
lintrshould object otherwise, even for comments. Try to break lines at semantic boundaries: ends of sentences or phrases. Long lines in source code files make it more difficult to see and comment on diffs in pull requests.In prose text chunks, Quarto ignores single line breaks, so you should also line-break your prose text in .qmd files to keep them under 80 characters.
You can configure RStudio’s settings to display the 80-character margin.