Code
install.packages("dplyr")
install.packages("ggplot2")
install.packages("tidyr")
install.packages("flextable")
install.packages("purrr")
install.packages("checkdown")Martin Schweinberger


This tutorial is the second in the LADAL R series. It picks up where Getting Started with R and RStudio left off and introduces the programming side of R: how to write code that makes decisions, repeats itself, and encapsulates reusable logic. These are the tools that transform R from a calculator into a real programming environment — the tools you reach for when you want to automate a task, process many files at once, or build a custom analysis pipeline.
The tutorial uses linguistic examples throughout: text cleaning, corpus processing, token counting, and data wrangling tasks typical of language research. By the end, you will be able to write your own functions, process data in loops, and apply the same operation efficiently across many groups or files.
Before working through this tutorial, please complete:
You should be comfortable with objects, vectors, data frames, and basic dplyr operations before continuing.
if/else, ifelse(), dplyr::case_when()for loops — iterating over vectors, lists, and fileswhile loops — condition-driven iterationapply family — sapply(), lapply(), apply()purrr — map() and its variantstryCatch() for robust codeInstall required packages (once only):
Load packages and set options:
library(dplyr) # data manipulation
library(ggplot2) # data visualisation
library(tidyr) # data reshaping
library(flextable) # formatted tables
library(purrr) # functional programming tools
library(checkdown) # interactive exercises
options(stringsAsFactors = FALSE)
options(scipen = 100)
options(max.print = 100)
set.seed(42)We will use a small simulated dataset throughout this tutorial — a collection of text samples with associated metadata:
# A small corpus of text samples
corpus <- data.frame(
doc_id = paste0("doc", 1:12),
register = rep(c("Academic", "News", "Fiction"), each = 4),
text = c(
"The syntactic properties of embedded clauses remain poorly understood.",
"Phonological alternations in unstressed syllables exhibit considerable variation.",
"Discourse coherence is maintained through a variety of cohesive devices.",
"The morphological complexity of agglutinative languages poses theoretical challenges.",
"Scientists announced a major breakthrough in renewable energy storage yesterday.",
"Local authorities confirmed that road closures will affect the city centre this weekend.",
"The prime minister addressed parliament amid growing calls for electoral reform.",
"Unemployment figures fell sharply in the third quarter according to new statistics.",
"She had not expected the letter to arrive so soon, or to contain such news.",
"The old house creaked and groaned as the storm gathered strength outside.",
"He said nothing for a long time, watching the rain trace patterns on the glass.",
"By morning the fog had lifted and the valley lay green and still below them."
),
n_tokens = c(11, 10, 12, 11, 14, 16, 13, 14, 17, 15, 18, 16),
year = c(2019, 2020, 2021, 2022, 2019, 2020, 2021, 2022,
2019, 2020, 2021, 2022),
stringsAsFactors = FALSE
)What you’ll learn: How to make R take different actions depending on the data — the foundation of any decision-making code
Key functions: if, else, else if, ifelse(), dplyr::case_when()
Why it matters: Real data is messy and varied — conditional logic lets your code respond intelligently to what it finds
if / else StatementsAn if statement runs a block of code only when a condition is TRUE. The optional else block runs when it is FALSE.
Corpus is large enough for analysis: 12 documents.
Chain multiple conditions with else if:
Average token count: 13.9 → Complexity: moderate
if Requires a Single TRUE or FALSE
The condition inside if() must evaluate to exactly one logical value. In R 4.2 and later, passing a vector of logicals is a hard error (in older versions it was a warning that used only the first element). Either way, it is never what you want.
Error: the condition has length > 1
Use any() or all() when you need to reduce a logical vector to a single value:
ifelse() — Vectorised Conditionalifelse() applies a condition to an entire vector and returns a vector of results — one for each element. This makes it ideal for creating or recoding columns:
dplyr::case_when() — Multiple ConditionsWhen you need more than two categories, case_when() is far cleaner than nested ifelse() calls. It works like a series of if/else if conditions, evaluated top to bottom:
Early Middle Recent
3 6 3
case_when() Evaluation Order
Conditions are tested top to bottom and the first match wins. Always put more specific conditions before less specific ones. The final TRUE ~ "value" acts as a catch-all default (like else) — it is good practice to always include one.
switch() — Selecting Among Named Optionsswitch() is useful when you have a single variable that can take one of several known values, and you want to map each value to a different result or action:
describe_register <- function(reg) {
switch(reg,
"Academic" = "Formal; high lexical density; passive constructions common",
"News" = "Neutral; inverted pyramid structure; quotations frequent",
"Fiction" = "Varied; narrative voice; dialogue and description",
"Unknown register"
)
}
describe_register("Academic")[1] "Formal; high lexical density; passive constructions common"
[1] "Neutral; inverted pyramid structure; quotations frequent"
[1] "Unknown register"
Q1. What is the key difference between if and ifelse() in R?
Q2. In a case_when() call, what does the final TRUE ~ "Unknown" line do?
Q3. You want to add a column pos_class that is \"function\" when word is in c(\"the\", \"a\", \"of\", \"in\") and \"content\" otherwise. Which code is correct?
for LoopsWhat you’ll learn: How to repeat a block of code for each element of a sequence or list
Key concepts: Loop variable, iteration, pre-allocation, seq_along()
Why it matters: Loops automate repetitive tasks — processing multiple files, computing statistics per document, or building up results iteratively
A for loop iterates over a sequence, executing its body once per element. The loop variable takes each element’s value in turn:
Academic : 4 documents
News : 4 documents
Fiction : 4 documents
When you need both the element and its position, loop over indices using seq_along(). This is safer than 1:length(x) because it handles zero-length vectors correctly:
Word 1: syntax (6 characters)
Word 2: morphology (10 characters)
Word 3: phonology (9 characters)
Word 4: pragmatics (10 characters)
Word 5: semantics (9 characters)
The most important loop performance rule: pre-allocate your output object before the loop, then fill it by index. Growing a vector by appending inside a loop forces R to copy the entire vector on every iteration — catastrophically slow for large inputs.
# BAD: growing inside the loop (slow for large n)
results_slow <- c()
for (i in seq_along(words)) {
results_slow <- c(results_slow, nchar(words[i])) # full copy each time!
}
# GOOD: pre-allocate, then fill by index
results_fast <- integer(length(words))
for (i in seq_along(words)) {
results_fast[i] <- nchar(words[i])
}
results_fast[1] 6 10 9 10 9
Here we loop over registers, compute summary statistics for each, and collect the results in a pre-allocated list:
registers <- unique(corpus$register)
summaries <- vector("list", length(registers)) # pre-allocate a list
names(summaries) <- registers
for (reg in registers) {
subset_df <- corpus[corpus$register == reg, ]
summaries[[reg]] <- data.frame(
register = reg,
n_docs = nrow(subset_df),
mean_tok = round(mean(subset_df$n_tokens), 1),
sd_tok = round(sd(subset_df$n_tokens), 2),
min_tok = min(subset_df$n_tokens),
max_tok = max(subset_df$n_tokens)
)
}
# Combine list of data frames into one
do.call(rbind, summaries) %>%
flextable() %>%
flextable::set_table_properties(width = .85, layout = "autofit") %>%
flextable::theme_zebra() %>%
flextable::fontsize(size = 12) %>%
flextable::fontsize(size = 12, part = "header") %>%
flextable::align_text_col(align = "center") %>%
flextable::set_caption(caption = "Token statistics per register computed with a for loop.") %>%
flextable::border_outer()register | n_docs | mean_tok | sd_tok | min_tok | max_tok |
|---|---|---|---|---|---|
Academic | 4 | 11.0 | 0.82 | 10 | 12 |
News | 4 | 14.2 | 1.26 | 13 | 16 |
Fiction | 4 | 16.5 | 1.29 | 15 | 18 |
One of the most practical uses of for loops in corpus linguistics is processing many text files in a folder:
# List all .txt files in a folder
txt_files <- list.files(path = "data/corpus/",
pattern = "\\.txt$",
full.names = TRUE)
# Pre-allocate results
results <- data.frame(
filename = character(length(txt_files)),
n_chars = integer(length(txt_files)),
n_lines = integer(length(txt_files)),
stringsAsFactors = FALSE
)
for (i in seq_along(txt_files)) {
text <- readLines(txt_files[i], warn = FALSE)
results$filename[i] <- basename(txt_files[i])
results$n_chars[i] <- sum(nchar(text))
results$n_lines[i] <- length(text)
}
head(results)break and nextTwo special keywords control loop flow:
break: exit the loop immediatelynext: skip to the next iteration (like continue in other languages)Long documents only:
doc5 - 14 tokens
doc6 - 16 tokens
doc7 - 13 tokens
doc8 - 14 tokens
doc9 - 17 tokens
doc10 - 15 tokens
doc11 - 18 tokens
doc12 - 16 tokens
First Academic document:
doc1 : The syntactic properties of embedded clauses remai ...
for LoopsLoops can be nested — the inner loop runs completely for each iteration of the outer loop:
Documents per register × era:
Academic × Early : 1
Academic × Middle : 2
Academic × Recent : 1
News × Early : 1
News × Middle : 2
News × Recent : 1
Fiction × Early : 1
Fiction × Middle : 2
Fiction × Recent : 1
Before writing a loop, always ask: does a vectorised function or dplyr verb already do this? Vectorised operations in R are implemented in C and run orders of magnitude faster than R-level loops.
[1] 70 81 72 85 80 88 80 83 75 73 79 76
# A tibble: 3 × 2
register mean_tok
<chr> <dbl>
1 Academic 11
2 Fiction 16.5
3 News 14.2
Loops shine when: (a) each iteration depends on the result of the previous one, (b) you are reading/writing files, or (c) no vectorised alternative exists.
for Loops
Q1. Why should you pre-allocate your output vector before a for loop rather than growing it with c() inside the loop?
Q2. What does next do inside a for loop?
Q3. Why is seq_along(x) preferred over 1:length(x) when looping over a vector x?
while LoopsWhat you’ll learn: How to write loops that run until a condition changes rather than for a fixed number of iterations
Key concepts: Loop condition, infinite loops, break as a safety exit
When to use: Convergence algorithms, reading streams of data, retrying failed operations
A while loop runs its body as long as its condition remains TRUE. Use it when the number of iterations is not known in advance.
Reached 58 tokens after 5 documents.
Here we simulate reading tokens from a stream until we hit a sentence boundary (a token ending in .):
tokens <- c("The", "quick", "brown", "fox", "jumps", ".", "Over", "the", "lazy")
sentence <- character(0)
j <- 0
while (j < length(tokens)) {
j <- j + 1
current <- tokens[j]
sentence <- c(sentence, current)
if (grepl("\\.$", current)) break # stop at sentence boundary
}
cat("First sentence:", paste(sentence, collapse = " "), "\n")First sentence: The quick brown fox jumps .
A while loop runs forever if its condition never becomes FALSE. Always ensure:
break safety exit is included for unexpected situationsConverged to 0.9698 after 44 iterations.
If you accidentally create an infinite loop, press Escape in the Console, or click the Stop button (red square) in the Console toolbar. RStudio will interrupt the running code. If that fails, use Session → Interrupt R from the menu.
while Loops
Q1. When is a while loop more appropriate than a for loop?
Q2. What is the risk of writing while (TRUE) { ... } without a break statement inside the body?
What you’ll learn: How to write your own reusable functions in R — the single most important skill for writing clean, maintainable code
Key concepts: Function definition, arguments, default values, return values, scope, documentation
Why it matters: Functions eliminate copy-paste errors, make your intentions explicit, and make code testable and shareable
The rule of thumb: if you have written the same block of code more than twice, it should be a function.
# General template:
# my_function <- function(required_arg, optional_arg = default_value) {
# # body: code that does the work
# return(result) # optional: last expression is returned automatically
# }
# A minimal example
greet_language <- function(language) {
paste("Hello from", language, "linguistics!")
}
greet_language("computational")[1] "Hello from computational linguistics!"
[1] "Hello from corpus linguistics!"
Arguments without a default are required — omitting them raises an error. Arguments with a default are optional and use their default when not supplied:
# type_token_ratio: required x (character vector of tokens), optional lowercase
ttr <- function(tokens, lowercase = TRUE) {
if (lowercase) tokens <- tolower(tokens)
n_tokens <- length(tokens)
n_types <- length(unique(tokens))
n_types / n_tokens
}
sample_tokens <- c("The", "cat", "sat", "on", "the", "mat", "the", "cat")
ttr(sample_tokens) # lowercase = TRUE (default)[1] 0.625
[1] 0.75
A function automatically returns its last evaluated expression. Use return() explicitly for early exits or when clarity matters:
# Early return when input is invalid
safe_ttr <- function(tokens, lowercase = TRUE) {
if (length(tokens) == 0) {
warning("Empty token vector supplied — returning NA.")
return(NA_real_)
}
if (!is.character(tokens)) {
stop("tokens must be a character vector.")
}
if (lowercase) tokens <- tolower(tokens)
length(unique(tokens)) / length(tokens)
}
safe_ttr(character(0)) # triggers warning, returns NAWarning in safe_ttr(character(0)): Empty token vector supplied — returning NA.
[1] NA
[1] 0.625
Functions can only return one object, but that object can be a named list containing as many results as needed:
corpus_stats <- function(tokens, lowercase = TRUE) {
if (lowercase) tokens <- tolower(tokens)
list(
n_tokens = length(tokens),
n_types = length(unique(tokens)),
ttr = round(length(unique(tokens)) / length(tokens), 3),
longest = tokens[which.max(nchar(tokens))]
)
}
result <- corpus_stats(sample_tokens)
result$ttr[1] 0.625
[1] "the"
List of 4
$ n_tokens: int 8
$ n_types : int 5
$ ttr : num 0.625
$ longest : chr "the"
Variables created inside a function live only inside that function — they are invisible to (and cannot accidentally overwrite) the global environment:
[1] "hello world"
[1] FALSE
<<- Operator
If you genuinely need to modify a variable in the calling environment from inside a function (rare), use <<- instead of <-. This searches up the call stack to find the variable and modifies it there. However, this is considered bad practice in most data analysis code because it creates hidden side effects that make functions unpredictable. Prefer returning a value and assigning it explicitly.
roxygen2 StyleGood functions should be documented so that you (and colleagues) can understand them months later. The conventional format mirrors the roxygen2 package style:
#' Compute Type-Token Ratio
#'
#' @description
#' Calculates the type-token ratio (TTR) of a character vector of tokens.
#' TTR = number of unique word types / total number of tokens.
#'
#' @param tokens A character vector of tokens (words).
#' @param lowercase Logical. If TRUE (default), tokens are lowercased before
#' counting, so "The" and "the" count as the same type.
#'
#' @return A single numeric value between 0 and 1. Values closer to 1
#' indicate higher lexical diversity.
#'
#' @examples
#' ttr(c("the", "cat", "sat", "on", "the", "mat"))
#' ttr(c("The", "Cat", "sat"), lowercase = FALSE)
ttr <- function(tokens, lowercase = TRUE) {
if (length(tokens) == 0) return(NA_real_)
if (lowercase) tokens <- tolower(tokens)
length(unique(tokens)) / length(tokens)
}Here is a realistic example: a family of small, focused functions composed into a pipeline:
# Step 1: normalise whitespace and case
normalise_text <- function(text) {
text <- tolower(text)
text <- trimws(text)
gsub("\\s+", " ", text) # collapse multiple spaces
}
# Step 2: remove punctuation
remove_punct <- function(text) {
gsub("[[:punct:]]", "", text)
}
# Step 3: tokenise (split on whitespace)
tokenise <- function(text) {
strsplit(text, "\\s+")[[1]]
}
# Step 4: remove stopwords
remove_stopwords <- function(tokens,
stopwords = c("the","a","an","of","in","and","to","is")) {
tokens[!tokens %in% stopwords]
}
# Compose into a full pipeline
clean_and_tokenise <- function(text, stopwords = NULL) {
text <- normalise_text(text)
text <- remove_punct(text)
tokens <- tokenise(text)
if (!is.null(stopwords)) tokens <- remove_stopwords(tokens, stopwords)
tokens
}
# Apply to one document
example_text <- "The syntactic properties of embedded clauses remain poorly understood."
clean_and_tokenise(example_text,
stopwords = c("the", "a", "an", "of", "in", "and", "to", "is"))[1] "syntactic" "properties" "embedded" "clauses" "remain"
[6] "poorly" "understood"
doc_id register n_tokens content_tokens
1 doc1 Academic 11 7
2 doc2 Academic 10 7
3 doc3 Academic 12 7
4 doc4 Academic 11 7
5 doc5 News 14 8
6 doc6 News 16 12
Q1. A function has no explicit return() statement. What does it return?
Q2. You write x <- 99 inside a function body. After calling the function, does x exist in the global environment?
Q3. Your function computes three things: n_tokens, n_types, and TTR. What is the best way to return all three?
apply FamilyWhat you’ll learn: How to apply a function to every element of a vector or list without writing an explicit loop
Key functions: sapply(), lapply(), apply()
Why it matters: The apply family is more concise than loops and often faster — it expresses intent clearly
The apply family of functions replaces many common loop patterns with a single, expressive call. They all share the same pattern: apply this function to each element of this object.
sapply() — Simplified Applysapply() applies a function to each element of a vector or list and simplifies the result to a vector or matrix if possible:
The syntactic properties of embedded clauses remain poorly understood.
70
Phonological alternations in unstressed syllables exhibit considerable variation.
81
Discourse coherence is maintained through a variety of cohesive devices.
72
The morphological complexity of agglutinative languages poses theoretical challenges.
85
Scientists announced a major breakthrough in renewable energy storage yesterday.
80
Local authorities confirmed that road closures will affect the city centre this weekend.
88
# Type-token ratio for a list of token vectors
token_lists <- list(
doc1 = c("the", "cat", "sat", "on", "the", "mat"),
doc2 = c("a", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"),
doc3 = c("to", "be", "or", "not", "to", "be")
)
sapply(token_lists, ttr) # returns a named numeric vector doc1 doc2 doc3
0.8333333 1.0000000 0.6666667
Use an anonymous function (a function defined inline without a name) for more complex operations:
The syntactic properties of embedded clauses remain poorly understood.
7
Phonological alternations in unstressed syllables exhibit considerable variation.
7
Discourse coherence is maintained through a variety of cohesive devices.
7
The morphological complexity of agglutinative languages poses theoretical challenges.
7
Scientists announced a major breakthrough in renewable energy storage yesterday.
8
Local authorities confirmed that road closures will affect the city centre this weekend.
12
The prime minister addressed parliament amid growing calls for electoral reform.
9
Unemployment figures fell sharply in the third quarter according to new statistics.
8
She had not expected the letter to arrive so soon, or to contain such news.
7
The old house creaked and groaned as the storm gathered strength outside.
7
He said nothing for a long time, watching the rain trace patterns on the glass.
9
By morning the fog had lifted and the valley lay green and still below them.
7
lapply() — List Applylapply() always returns a list, making it safer when results have different lengths or types:
$doc1
[1] "the" "cat" "sat" "on" "mat"
$doc2
[1] "a" "quick" "brown" "fox" "jumps" "over" "the" "lazy" "dog"
$doc3
[1] "to" "be" "or" "not"
$doc1
[1] "the" "cat" "sat" "on" "mat"
$doc2
[1] "a" "quick" "brown" "fox" "jumps" "over" "the" "lazy" "dog"
$doc3
[1] "to" "be" "or" "not"
apply() — Matrix/Data Frame Applyapply() operates on matrices or data frames, applying a function across rows (MARGIN = 1) or columns (MARGIN = 2):
n_tokens content_tokens
13.91667 9.25000
doc1 doc2 doc3 doc4 doc5 doc6
18 17 19 18 22 28
sapply() and lapply()Function | Input | Output | Use when |
|---|---|---|---|
sapply() | vector or list | vector/matrix (simplified) or list if simplification fails | results are all the same type and length |
lapply() | vector or list | always a list | results differ in length or type; you always want a list |
apply() | matrix or data frame | vector or list | you want to summarise across rows or columns of a matrix |
purrrWhat you’ll learn: How to use purrr::map() and its variants as a modern, consistent alternative to the apply family
Key functions: map(), map_chr(), map_dbl(), map_df(), map2(), walk()
Why it matters: purrr functions have consistent, predictable behaviour and integrate cleanly with dplyr pipelines
The purrr package provides a family of map() functions that replace the apply family with a more consistent interface. Every map() function takes a list or vector and applies a function to each element.
map() and Type-Specific Variantsmap() always returns a list. Type-specific variants guarantee a particular output type and fail informatively if the results do not match:
$doc1
[1] 6
$doc2
[1] 9
$doc3
[1] 6
doc1 doc2 doc3
0.8333333 1.0000000 0.6666667
doc1 doc2 doc3
6 9 6
doc1 doc2 doc3
"the cat" "a quick" "to be"
map_df() — Map to a Data Framemap_df() (or map() |> list_rbind()) is extremely useful for applying a function that returns a data frame to each element and binding the results together:
doc n_tokens n_types ttr
1 doc1 6 5 0.833
2 doc2 9 9 1.000
3 doc3 6 4 0.667
map2() — Map Over Two Inputs Simultaneouslymap2() applies a function to corresponding elements of two vectors or lists:
text_A : TTR = 1
text_B : TTR = 1
text_C : TTR = 0.75
[1] 1.00 1.00 0.75
walk() — Map for Side Effectswalk() is like map() but is used when you want the side effect (printing, writing a file, making a plot) rather than the return value. It invisibly returns the input:
Register: Academic | Docs: 4 | Mean tokens: 11.0
Register: Fiction | Docs: 4 | Mean tokens: 16.5
Register: News | Docs: 4 | Mean tokens: 14.2
apply and purrr
Q1. What is the difference between sapply() and lapply()?
Q2. When would you use purrr::walk() instead of purrr::map()?
What you’ll learn: How to write code that handles errors and warnings gracefully rather than crashing
Key functions: tryCatch(), try(), stop(), warning(), message()
Why it matters: When processing many files or documents, a single error should not halt your entire pipeline
Use stop(), warning(), and message() to communicate problems from inside your functions:
compute_ttr <- function(tokens) {
if (!is.character(tokens)) stop("tokens must be a character vector")
if (length(tokens) == 0) warning("Empty vector — returning NA")
if (length(tokens) < 10) message("Note: TTR is unreliable for short texts")
if (length(tokens) == 0) return(NA_real_)
length(unique(tokens)) / length(tokens)
}
compute_ttr(c("the", "cat", "sat")) # message: short textNote: TTR is unreliable for short texts
[1] 1
tryCatch() — Handle Errors GracefullytryCatch() lets you intercept errors, warnings, and messages, and decide what to do instead of crashing:
# Without tryCatch: one bad input crashes everything
safe_ttr <- function(tokens) {
tryCatch(
expr = compute_ttr(tokens),
error = function(e) {
cat("Error in compute_ttr:", conditionMessage(e), "\n")
NA_real_
},
warning = function(w) {
cat("Warning:", conditionMessage(w), "\n")
NA_real_
}
)
}
safe_ttr(c("the", "cat", "sat", "on", "the", "mat")) # normalNote: TTR is unreliable for short texts
[1] 0.8333333
Error in compute_ttr: tokens must be a character vector
[1] NA
Warning: Empty vector — returning NA
[1] NA
tryCatch() Across a PipelineThis pattern is invaluable when processing many documents or files — one bad item should not stop the whole run:
Note: TTR is unreliable for short texts
Error in compute_ttr: tokens must be a character vector
Warning: Empty vector — returning NA
Note: TTR is unreliable for short texts
[1] 0.8333333 NA NA 1.0000000
Q1. What is the difference between stop(), warning(), and message() inside a function?
Q2. Why is wrapping a function call in tryCatch() useful when processing a large number of files or documents?
A concise guide to writing better R code: when to loop, when to vectorise, how to name and document functions, and how to structure growing code projects
Situation | Best tool |
|---|---|
Apply the same operation to every element of a vector | Vectorised operation (e.g., nchar(), tolower(), arithmetic) |
Apply the same operation to each group in a data frame | dplyr::group_by() + summarise() or mutate() |
Apply a function to each element and collect results | sapply() / lapply() / purrr::map() |
Iterate when each step depends on the previous result | for loop with pre-allocated output |
Number of iterations unknown; stop when condition met | while loop (with break safety exit) |
Apply a function for its side effects (print, save, plot) | purrr::walk() or a for loop |
Handle different cases of a single categorical variable | ifelse() / case_when() / switch() |
clean_and_tokenise_and_count_and_plot() is a sign it should be four functionsclean_text(), compute_ttr(), plot_frequency() — not myFunc() or data2()stop() at the top of the function body for invalid arguments# Good: clear structure, consistent indentation, descriptive names
compute_register_stats <- function(data, group_col = "register") {
data %>%
dplyr::group_by(.data[[group_col]]) %>%
dplyr::summarise(
n = dplyr::n(),
mean_tok = round(mean(n_tokens), 1),
sd_tok = round(sd(n_tokens), 2),
.groups = "drop"
)
}
# Bad: cryptic names, no whitespace, no structure
f<-function(d,g="register"){d%>%group_by(.data[[g]])%>%summarise(n=n(),m=round(mean(n_tokens),1))}Don’t Repeat Yourself. If you catch yourself copy-pasting a block of code and changing one value, that block should be a function parameterised by that value. Code duplication multiplies the places you must update when requirements change, and multiplies the opportunities for inconsistency.
# BEFORE: copy-pasted three times with minor changes
academic_ttr <- sum(corpus$register == "Academic") |> ...
news_ttr <- sum(corpus$register == "News") |> ...
fiction_ttr <- sum(corpus$register == "Fiction") |> ...
# AFTER: one function, called three times
get_register_ttr <- function(data, reg) { ... }
sapply(c("Academic", "News", "Fiction"), get_register_ttr, data = corpus)Schweinberger, Martin. 2026. Working with R: Control Flow, Functions, and Programming. Brisbane: The University of Queensland. url: https://ladal.edu.au/tutorials/workingwithr/workingwithr.html (Version 2026.02.19).
@manual{schweinberger2026workingwithr,
author = {Schweinberger, Martin},
title = {Working with R: Control Flow, Functions, and Programming},
note = {https://ladal.edu.au/tutorials/workingwithr/workingwithr.html},
year = {2026},
organization = {The University of Queensland, Australia. School of Languages and Cultures},
address = {Brisbane},
edition = {2026.02.19}
}
R version 4.4.2 (2024-10-31 ucrt)
Platform: x86_64-w64-mingw32/x64
Running under: Windows 11 x64 (build 26200)
Matrix products: default
locale:
[1] LC_COLLATE=English_United States.utf8
[2] LC_CTYPE=English_United States.utf8
[3] LC_MONETARY=English_United States.utf8
[4] LC_NUMERIC=C
[5] LC_TIME=English_United States.utf8
time zone: Australia/Brisbane
tzcode source: internal
attached base packages:
[1] stats graphics grDevices datasets utils methods base
other attached packages:
[1] purrr_1.0.4 flextable_0.9.7 tidyr_1.3.2 ggplot2_4.0.2
[5] dplyr_1.2.0 checkdown_0.0.13
loaded via a namespace (and not attached):
[1] utf8_1.2.4 generics_0.1.3 fontLiberation_0.1.0
[4] renv_1.1.1 xml2_1.3.6 digest_0.6.39
[7] magrittr_2.0.3 evaluate_1.0.3 grid_4.4.2
[10] RColorBrewer_1.1-3 fastmap_1.2.0 jsonlite_1.9.0
[13] zip_2.3.2 scales_1.4.0 fontBitstreamVera_0.1.1
[16] codetools_0.2-20 textshaping_1.0.0 cli_3.6.4
[19] rlang_1.1.7 fontquiver_0.2.1 litedown_0.9
[22] commonmark_2.0.0 withr_3.0.2 yaml_2.3.10
[25] gdtools_0.4.1 tools_4.4.2 officer_0.6.7
[28] uuid_1.2-1 vctrs_0.7.1 R6_2.6.1
[31] lifecycle_1.0.5 htmlwidgets_1.6.4 ragg_1.3.3
[34] pkgconfig_2.0.3 pillar_1.10.1 gtable_0.3.6
[37] data.table_1.17.0 glue_1.8.0 Rcpp_1.0.14
[40] systemfonts_1.2.1 xfun_0.56 tibble_3.2.1
[43] tidyselect_1.2.1 rstudioapi_0.17.1 knitr_1.51
[46] farver_2.1.2 htmltools_0.5.9 rmarkdown_2.30
[49] compiler_4.4.2 S7_0.2.1 askpass_1.2.1
[52] markdown_2.0 openssl_2.3.2