Writing Functions & Packages
Reviewed & published by Brayan K
A package is R's unit for organising and sharing reusable code — bundling well-written, documented functions so you (and others) can install and reuse them across projects.
Part of the free R course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll write robust, validated functions, document them with roxygen comments, compose small functions into bigger tools, and understand the workflow that turns them into a sharable package.
What You'll Learn in This Lesson
- How to validate inputs with stop()
- How to document functions with roxygen #' comments
- How to compose small functions into bigger ones
- The difference between install.packages() and library()
- The usethis + devtools package workflow
- Why returning a list bundles multiple results
1️⃣ Robust, Documented Functions
A production-quality function validates its inputs (failing clearly with stop()), stays vectorized, and carries roxygen #' documentation describing its parameters and return value.
# A well-written function validates input and documents its behaviour.
#' Convert a temperature from Celsius to Fahrenheit.
#' @param celsius numeric vector of temperatures
#' @return numeric vector in Fahrenheit
to_fahrenheit <- function(celsius) {
if (!is.numeric(celsius)) {
stop("celsius must be numeric") # fail loudly with a clear message
}
celsius * 9 / 5 + 32 # vectorized: works on whole vectors
}
to_fahrenheit(c(0, 37, 100)) # -> [1] 32.0 98.6 212.02️⃣ Composing Small Functions
Big tools are built from small ones. Each function does one job; you combine them. Returning a named list lets a function hand back several results at once.
# Build bigger tools by composing small functions.
celsius_to_f <- function(c) c * 9 / 5 + 32
describe <- function(x) {
list(min = min(x), max = max(x), mean = round(mean(x), 1))
}
temps_c <- c(15, 22, 30)
temps_f <- celsius_to_f(temps_c)
describe(temps_f) # a named list summary
# $min [1] 59
# $max [1] 86
# $mean [1] 72.23️⃣ From Functions to a Package
Packages are how R shares code. The usethis + devtools workflow scaffolds, documents, and checks one. And using any package is always the same two steps: install.packages() once, library() each session.
# To turn functions into a sharable PACKAGE, the usethis + devtools
# workflow scaffolds everything. (Run these in RStudio, once.)
# install.packages(c("devtools", "usethis"))
# usethis::create_package("mytools") # makes the package skeleton
# usethis::use_r("convert") # creates R/convert.R for your functions
# Put functions (with #' roxygen comments) in R/, then:
# devtools::document() # builds help files from #' comments
# devtools::load_all() # load the package while developing
# devtools::check() # run R's quality checks
# Using ANY package follows the same two steps you've seen:
# install.packages("dplyr") # once, to download it
library(dplyr) # each session, to load it
"package loaded" # -> [1] "package loaded"Your turn. Fill in the ___ blank, run it, and compare with the expected output.
# 🎯 YOUR TURN — replace each ___ then run it.
#' Return the share of values above a threshold, as a percentage.
percent_above <- function(x, threshold) {
if (!is.numeric(x)) stop("x must be numeric")
mean(x ___ threshold) * 100 # 👉 replace ___ with >
}
scores <- c(55, 70, 82, 90, 65, 100)
cat("Above 70:", percent_above(scores, 70), "%\n")
# ✅ Expected output:
# Above 70: 50 %
#
# (3 of 6 values are strictly above 70 -> 50%)Mini-Challenge: A Reusable Toolkit
Write both functions from the outline, run them, and check against the example output. Two small, validated, reusable functions are the seed of any package.
# 🎯 MINI-CHALLENGE: a tiny reusable toolkit.
#
# 1. Write normalize(x): rescale a numeric vector to 0-1 using
# (x - min(x)) / (max(x) - min(x)). Validate that x is numeric.
# 2. Write summarise_vec(x): return a named list with n, mean, and sd.
# 3. Test both on c(2, 4, 6, 8, 10):
# - print normalize(...)
# - print summarise_vec(...)
#
# ✅ Example output:
# [1] 0.00 0.25 0.50 0.75 1.00
# $n [1] 5 / $mean [1] 6 / $sd [1] 3.16...
# your code hereCommon Errors (and the fix)
- "could not find function" — the package isn't loaded. Add library(pkg) (after installing it once).
- library() on a missing package — install it first with install.packages("pkg"), then load it.
- Silent wrong answers — no input validation. Add stop() guards so bad input fails loudly.
- Docs out of date — you edited #' comments but forgot devtools::document() to rebuild help files.
Pro Tips
- 💡 Test as you build: the testthat package lets you write automated tests for your functions.
- 💡 Share via GitHub: others can install your package with devtools::install_github("user/repo").
- 💡 One job per function: small, focused functions are easier to validate, document, and reuse.
📋 Quick Reference — Functions & Packages
| Task | R Syntax |
|---|---|
| Validate input | if (!is.numeric(x)) stop("...") |
| Document | #' @param x ... |
| Install once | install.packages("pkg") |
| Load each session | library(pkg) |
| New package | usethis::create_package() |
| Build docs | devtools::document() |
🎉 Lesson Complete!
- ✅ Validate inputs and fail loudly with stop()
- ✅ Document functions with roxygen #' comments
- ✅ Compose small functions into bigger tools
- ✅ install.packages() once, library() each session
- ✅ usethis + devtools build a sharable package
Practice quiz
Which function makes a function fail loudly on bad input?
- warn()
- stop()
- print()
- cat()
Answer: stop(). stop() halts execution with a clear error message for invalid input.
What does install.packages("dplyr") do?
- Loads it into the session
- Downloads and installs the package once
- Documents it
- Deletes it
Answer: Downloads and installs the package once. install.packages() downloads and installs a package; you do it once.
What does library(dplyr) do?
- Installs the package
- Uninstalls it
- Loads an installed package into the session
- Publishes it to CRAN
Answer: Loads an installed package into the session. library() loads an already-installed package each session so its functions are available.
What are the #' comment lines above a function for?
- They are ignored entirely
- They speed up the function
- They are normal comments
- roxygen documentation
Answer: roxygen documentation. #' lines are roxygen2 comments that generate the function's help page.
Which function scaffolds a new package skeleton?
- usethis::create_package()
- make_package()
- new.package()
- devtools::build()
Answer: usethis::create_package(). usethis::create_package() sets up the package folder structure.
Which command builds help files from #' comments?
- library()
- devtools::document()
- install.packages()
- use_r()
Answer: devtools::document(). devtools::document() reads roxygen comments and generates the help files.
What does the @param tag describe in a roxygen comment?
- The return value
- The package name
- A function argument
- The author
Answer: A function argument. @param documents one of the function's arguments.
What causes 'could not find function' for a package's function?
- The package isn't loaded with library()
- Too many arguments
- A typo in numbers
- The function returns NULL
Answer: The package isn't loaded with library(). You must load the installed package with library(pkg) before using its functions.
Why return a named list from a function?
- Lists are required
- To bundle several results in one object
- To make it run faster
- Lists print prettier
Answer: To bundle several results in one object. A named list lets a function hand back several results at once.
How can others install your package straight from GitHub?
- library(github)
- install.packages(github)
- use_github()
- devtools::install_github("user/repo")
Answer: devtools::install_github("user/repo"). devtools::install_github() installs a package directly from a GitHub repo.
Continue this course
- Previous: Linear Models (lm)
- Next: Matrix Algebra (%*%, solve, t) — Multiply, transpose, invert, and solve linear systems with matrices
- Quick reference: R cheat sheet › I/O
Frequently asked questions
How do I write a robust, reusable function in R?
Start with a clear name and a single responsibility. Validate inputs early with checks like if (!is.numeric(x)) stop("...") so the function fails loudly with a helpful message instead of producing wrong results. Keep the body vectorized where possible so it works on whole vectors. Document it with roxygen comments (#' lines describing parameters and the return value). Finally, return the result as the last expression or with return(). Small, well-validated, documented functions are easy to test and reuse.
What's the difference between install.packages() and library()?
install.packages("name") downloads and installs a package onto your computer — you do this once per package (and again to update it). library(name) loads an already-installed package into your current R session so its functions become available — you do this every session, at the top of your script. A frequent beginner error is calling library() on something never installed, which errors; or forgetting library() and getting 'could not find function'.
What does it take to turn my functions into a package?
An R package is a structured folder: your functions live in an R/ directory, documentation is generated from roxygen comments, and a DESCRIPTION file records metadata. The modern toolkit — usethis and devtools — automates the scaffolding: create_package() sets up the skeleton, document() builds the help files, load_all() loads it while you develop, and check() runs R's quality checks. You don't have to publish to CRAN; a package is just as useful shared as a file or installed from GitHub.
What are those #' comment lines above functions?
Those are roxygen2 comments — special documentation that starts with #'. Tags like @param describe each argument and @return describes the output. When you run devtools::document(), roxygen2 reads these comments and generates the proper R help files, so users can type ?your_function and see a real help page. Writing roxygen comments as you go is a good habit even before you make a package: it forces you to clarify what each function does and expects.