SECTION 1
Historical Context & Motivation
The practice of documenting code is nearly as old as programming itself, yet the discipline of function documentation — structured annotations that describe a function's purpose, parameters, and return values — evolved considerably over the decades. Early programmers used inline comments sparingly, relying instead on external manuals and printed reference cards to explain what their code did. As software systems grew in complexity throughout the 1970s and 1980s, the gap between what code did and what developers understood about it became a serious engineering problem. The idea that documentation should live alongside the source code, rather than in separate binders, gradually gained traction across multiple language ecosystems.
1976 • Literate Programming Concept
1995 • Javadoc Standardizes Inline Docs
2000 • R's Built-in .Rd Format
2011 • roxygen2 Revolutionizes R Documentation
2020s • Modern R Ecosystem Standards
The central question this lesson addresses is deceptively simple: how do you write R functions that explain themselves? Without clear documentation, even well-written code becomes opaque within weeks — to collaborators and to your future self. Understanding both basic comment conventions and the roxygen2 annotation system equips you to produce code that is not only functional but also comprehensible, reusable, and suitable for professional-grade package development.
SECTION 2
Core Principles of Function Documentation
Effective function documentation in R rests on several foundational principles that apply whether you are writing a quick analysis script or building a CRAN package. These principles guide you in choosing what to document, where to place annotations, and how to structure them so that both human readers and automated tools can parse your intent. Mastering these ideas early will save you significant time as your projects scale from single-file scripts to multi-function packages.
Proximity Principle
Describe the Contract
Structured Tags Over Prose
Examples Are Documentation
Progressive Detail
KEY TAKEAWAY
SECTION 3
Anatomy of a Documented R Function
The diagram below illustrates the structure of a fully documented R function using roxygen2-style comments. Each colored region corresponds to a specific documentation element: the title, the description, parameter tags, the return tag, examples, and finally the function body itself. Notice how the roxygen2 block (lines beginning with #') immediately precedes the function assignment, maintaining the proximity principle.
#' prefix distinguishes roxygen2 annotations from ordinary comments.The critical detail to notice is the distinction between the #' (hash-apostrophe) prefix used by roxygen2 and the standard # prefix used for regular R comments. Regular comments are completely ignored by the documentation generation pipeline; they serve only as inline notes for developers reading the source. Roxygen2 comments, on the other hand, are parsed, validated, and converted into the .Rd files that R's help system displays when you call ?function_name.
SECTION 4
How Roxygen2 Processing Works
Understanding the roxygen2 pipeline helps clarify why structured tags matter. When you run devtools::document() or roxygen2::roxygenise(), the roxygen2 package scans every .R file in your R/ directory, extracts all blocks prefixed with #', parses the tags within them, and generates the corresponding .Rd documentation files in the man/ directory. This process is deterministic: each roxygen2 block maps to exactly one help page, and the tags within the block dictate the sections of that page.
The pipeline diagram makes an important architectural point: you never edit .Rd files by hand when using roxygen2. The man/ directory is entirely generated output. This means your single source of truth for both code and documentation is the .R file itself — a significant improvement over the legacy workflow where you had to maintain two separate files and manually keep them synchronized.
Plain Comments vs. Roxygen2 Comments
# This is a comment) are for internal developer notes — explaining tricky logic, marking TODOs, or annotating complex algorithms. Roxygen2 comments (#' This is a roxygen comment) are for external-facing documentation that will appear in help pages. Use both, but don't conflate their purposes.SECTION 5
Essential Roxygen2 Tags in Detail
While roxygen2 supports dozens of tags for advanced use cases — controlling namespaces, documenting S4 classes, specifying package-level metadata — this introduction focuses on the six tags you will use most frequently. Mastery of these six covers approximately 90% of day-to-day documentation needs in R programming.
| Tag | Syntax | Purpose | Required? |
|---|---|---|---|
| Title | #' Title text | The first sentence of the roxygen block becomes the title. Appears at the top of the help page. | Yes |
| Description | #' (paragraph after title) | A more detailed paragraph explaining what the function does. Separated from the title by a blank #' line. | Recommended |
@param | #' @param name Description | Documents a single parameter. Should include expected type, valid range, and default behavior if any. | Yes (per param) |
@return | #' @return Description | Describes the function's return value — its type, structure, and meaning. | Yes |
@examples | #' @examples\n#' f(1, 2) | Runnable R code demonstrating usage. Executed during R CMD check to verify correctness. | Strongly recommended |
@export | #' @export | Adds the function to the package's NAMESPACE, making it available to users. Without this, the function is internal. | For public functions |
Core roxygen2 tags for introductory function documentation
Writing Effective @param Descriptions
A high-quality @param description answers three questions: What type is it? (numeric vector, character string, logical, data frame), What does it represent? (a temperature in Celsius, a column name), and What constraints apply? (must be positive, length one, one of a specific set of values). Omitting any of these forces the user to read the function body to understand usage — the exact situation documentation is meant to prevent.
- Good:
@param alpha Numeric scalar in (0, 1). Significance level for the test. Default is 0.05. - Poor:
@param alpha The alpha value. - Good:
@param method Character string. One of "pearson", "kendall", or "spearman". - Poor:
@param method The method to use.
SECTION 6
Worked Example: Documenting a Statistical Function
Let us walk through documenting a function that computes a z-score normalization on a numeric vector. We will build the roxygen2 block tag by tag, explaining the reasoning behind each decision.
Documenting a z-score Normalization Function
- 1
Step 1 — Write the Function First
Start with a working function before writing documentation. Our function takes a numeric vector and returns the z-scores by subtracting the mean and dividing by the standard deviation:z_normalize <- function(x, na.rm = FALSE) { mu <- mean(x, na.rm = na.rm); sigma <- sd(x, na.rm = na.rm); return((x - mu) / sigma) }Function implemented and tested. - 2
Step 2 — Add the Title
The first line of the roxygen2 block is the title. It should be a concise, imperative phrase — no period at the end (per R conventions):#' Z-Score Normalize a Numeric VectorTitle: "Z-Score Normalize a Numeric Vector" - 3
Step 3 — Add the Description
After a blank roxygen2 line, write a paragraph explaining the function in more detail. Mention the formula and intended use:#' #' Centers and scales a numeric vector so that the result has mean 0 #' and standard deviation 1. Computes (x - mean(x)) / sd(x).Description paragraph added with formula reference. - 4
Step 4 — Document Each Parameter
Add a@paramtag for every formal argument. Include type, meaning, and constraints:#' @param x Numeric vector to be normalized. Must have length >= 2 and non-zero variance. #' @param na.rm Logical. Should NA values be stripped before computation? Default is FALSE.Two @param tags with type, meaning, and constraints. - 5
Step 5 — Specify the Return Value and Examples
Add@returnto describe the output and@exampleswith runnable code:#' @return A numeric vector of the same length as \code{x}, containing z-scores. #' #' @examples #' z_normalize(c(10, 20, 30)) #' z_normalize(c(1, NA, 3), na.rm = TRUE) #' #' @exportComplete roxygen2 block ready for roxygenise(). - 6
Step 6 — Assemble the Complete Documented Function
The final documented function looks like this:#' Z-Score Normalize a Numeric Vector #' #' Centers and scales a numeric vector so that the result has mean 0 #' and standard deviation 1. Computes (x - mean(x)) / sd(x). #' #' @param x Numeric vector to be normalized. Must have length >= 2 #' and non-zero variance. #' @param na.rm Logical. Should NA values be stripped before #' computation? Default is FALSE. #' #' @return A numeric vector of the same length as \code{x}, #' containing z-scores. #' #' @examples #' z_normalize(c(10, 20, 30)) #' z_normalize(c(1, NA, 3), na.rm = TRUE) #' #' @export z_normalize <- function(x, na.rm = FALSE) { mu <- mean(x, na.rm = na.rm) sigma <- sd(x, na.rm = na.rm) return((x - mu) / sigma) }Complete, professional-grade documented R function.
Common Pitfall
#'), it cannot associate the documentation block with the function. This is the single most common error beginners make.SECTION 7
Plain Comments vs. Roxygen2: Strengths & Limitations
Both plain R comments and roxygen2 annotations serve legitimate documentation purposes, but they target different audiences and workflows. Understanding when to use each — and their respective trade-offs — prevents over-engineering scripts while ensuring packages meet community standards. The table below provides a direct comparison across several dimensions relevant to professional R development.
| Dimension | Plain Comments (#) | Roxygen2 Comments (#') |
|---|---|---|
| Audience | Developers reading source code | End users via help pages, plus developers |
| Tooling | None — purely human-readable | Parsed by roxygen2, integrated with devtools, RStudio, pkgdown |
| Structure | Free-form, ad hoc | Standardized tags (@param, @return, etc.) |
| Help page generation | No — invisible to ?function_name | Yes — generates .Rd files automatically |
| Best for | Quick scripts, internal logic notes, TODOs | Package functions, shared codebases, any reusable function |
| Overhead | Minimal — just type a hash and write | Moderate — must learn tag syntax and follow conventions |
| Validation | None — comments can be wrong and nobody catches it | R CMD check verifies @param tags match function formals |
Comparison of plain comments vs. roxygen2 annotations
PRACTICAL GUIDELINE
SECTION 8
Connection to Advanced Documentation Practices
The introductory roxygen2 tags covered in this lesson form the foundation for a much richer documentation ecosystem. As your R packages grow in complexity, you will encounter advanced features that build directly on the concepts you have learned here. Understanding this trajectory helps you appreciate why getting the basics right matters — the advanced features extend the tag system rather than replacing it.
| Introductory Concept | Advanced Extension | When You'll Need It |
|---|---|---|
@param for individual params | @inheritParams to inherit param docs from another function | When multiple functions share the same parameter (e.g., data, verbose) |
@return for return value | @rdname to group related functions on one help page | When building S3/S4 method families or closely related utility functions |
| Free-text description | @section and @details for structured sections | When a function needs mathematical background, algorithm notes, or references |
@export for NAMESPACE | @importFrom and @import for dependency management | When your functions depend on other packages (dplyr, ggplot2, etc.) |
@examples section | Vignettes (vignette()) for long-form tutorials | When short examples can't convey a full workflow or use case |
From introductory documentation to advanced package development
Beyond roxygen2, the modern R documentation landscape includes tools like pkgdown (which converts your roxygen2-generated help pages into a full website), testthat (where examples evolve into formal unit tests), and R Markdown / Quarto for literate programming that blends narrative, code, and output. All of these tools build on or complement the roxygen2 foundation, so investing in clean roxygen2 habits now pays compounding dividends as your projects mature.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
#) and a roxygen2 comment (#'). In what specific situation would you choose one over the other? Provide a concrete example of each.Show answer
#) is ignored by both the R interpreter and the roxygen2 documentation generator — it exists solely for developers reading the source code. A roxygen2 comment (#') is also ignored by the R interpreter, but is parsed by the roxygen2 package and transformed into structured .Rd help files. Use plain comments for internal notes, such as # TODO: optimize this loop inside a function body. Use roxygen2 comments for user-facing documentation, such as #' @param n Integer. Number of bootstrap samples. above a function definition. The key distinction is audience: plain comments target maintainers; roxygen2 comments target users.Question 2: BASIC
fahrenheit_to_celsius <- function(temp_f) { return((temp_f - 32) * 5/9) }
Include a title, description, @param, @return, and @examples tags.Show answer
#' Convert Fahrenheit to Celsius
#'
#' Converts a temperature value from Fahrenheit to Celsius using the
#' standard formula: C = (F - 32) × 5/9.
#'
#' @param temp_f Numeric. Temperature in degrees Fahrenheit.
#'
#' @return A numeric value representing the temperature in degrees Celsius.
#'
#' @examples
#' fahrenheit_to_celsius(212) # boiling point: 100
#' fahrenheit_to_celsius(32) # freezing point: 0
#' @export
Notice the title has no trailing period (R convention), the @param specifies the type (Numeric) and meaning, the @return describes both the type and semantics, and the @examples include interpretable test cases with comments showing expected results.Question 3: INTERMEDIATE
#' Trimmed Mean.
#'
# Computes the trimmed mean of a vector.
#'
#' @param x the data
#' @param trim how much to trim
#' @return the trimmed mean
trimmed_mean <- function(x, trim = 0.1, na.rm = FALSE) {
mean(x, trim = trim, na.rm = na.rm)
}Show answer
#' Trimmed Mean. Error 2: The description line uses # instead of #', so roxygen2 won't parse it — it becomes an invisible plain comment. Fix: change to #' Computes the trimmed mean.... Error 3: The @param descriptions lack type information and constraints. Fix: @param x Numeric vector. and @param trim Numeric scalar in [0, 0.5]. Fraction of observations to trim from each end.. Error 4: The parameter na.rm is missing a @param tag entirely — R CMD check will produce a warning about undocumented arguments. Error 5: There is a blank line between the last roxygen2 line and the function definition, which breaks the association. Remove that blank line.Question 4: APPLIED
gc_content that takes a DNA sequence string (characters A, T, G, C) and returns the GC content as a proportion. Include input validation, roxygen2 documentation with all essential tags, and at least two examples — one normal case and one edge case.Show answer
#' Calculate GC Content of a DNA Sequence
#'
#' Computes the proportion of guanine (G) and cytosine (C) bases in a
#' DNA sequence string. GC content is a fundamental metric in genomics
#' that influences melting temperature and gene density.
#'
#' @param sequence Character string containing only the characters
#' A, T, G, and C (case-insensitive). Must have length >= 1.
#'
#' @return A numeric scalar between 0 and 1 representing the GC
#' content as a proportion.
#'
#' @examples
#' gc_content("ATGCGCTA") # 0.5
#' gc_content("AAAA") # 0.0 (edge case: no GC)
#' gc_content("GC") # 1.0 (edge case: all GC)
#'
#' @export
gc_content <- function(sequence) {
sequence <- toupper(sequence)
bases <- strsplit(sequence, "")[[1]]
stopifnot(all(bases %in% c("A", "T", "G", "C")))
gc_count <- sum(bases %in% c("G", "C"))
return(gc_count / length(bases))
}
This solution includes input validation via stopifnot(), case-insensitive handling via toupper(), a description that contextualizes the function within its domain, and examples covering both typical and edge cases. The @param tag specifies the type (Character string), valid values, and constraints.Question 5: CRITICAL THINKING
Show answer
SUMMARY
Lesson Summary
Summary
Function documentation in R spans two complementary systems: plain comments (prefixed with #) for internal developer notes, and roxygen2 annotations (prefixed with #') for structured, user-facing help pages. The roxygen2 system uses standardized tags — @param for parameters, @return for return values, @examples for runnable demonstrations, and @export for namespace management — to generate .Rd documentation files automatically via devtools::document().
Effective documentation follows the proximity principle (docs live next to code), describes the function's contract rather than its implementation, and provides progressive detail from a concise title through detailed parameter descriptions. Remember: the roxygen2 block must appear directly above the function definition with no intervening blank lines, and every @param tag should specify the parameter's type, meaning, and constraints to be truly useful.