R Programming • Software Craft and Communication

Refactoring — Refactor repeated code into functions

Eliminate duplication by extracting reusable functions, improving readability, maintainability, and correctness of R code.

A statistical scatter plot with code braces and a variable symbol for R programming.
Inside this lesson 10 sections 5 questions 4 figures

SECTION 1

Historical Context & Motivation

The practice of refactoring — restructuring existing code without changing its external behavior — emerged alongside the recognition that software is not written once and forgotten but continuously read, modified, and extended. In procedural and scripting languages such as R, the most pervasive form of technical debt is copy-paste duplication: identical or near-identical blocks of code scattered across a script, each performing the same computation with slightly different inputs. When a bug is discovered in one copy, every other copy must be located and patched — a fragile, error-prone process that violates the Don't Repeat Yourself (DRY) principle. Understanding the historical arc of refactoring helps explain why extracting repeated code into functions became a cornerstone of professional software development.

1972 • Structured Programming

Dijkstra, Dahl, and Hoare publish Structured Programming, arguing that programs should be decomposed into small, single-purpose procedures — laying the theoretical groundwork for function extraction.

1993 • R Language Created

Ross Ihaka and Robert Gentleman create R at the University of Auckland. As a functional language, R treats functions as first-class objects, making function extraction a natural refactoring technique.

1999 • Fowler's Refactoring Catalog

Martin Fowler publishes Refactoring: Improving the Design of Existing Code, cataloging "Extract Method" as the single most common refactoring pattern and formalizing the discipline of behavior-preserving transformation.

2014 • Tidyverse & Functional R

Hadley Wickham's tidyverse ecosystem promotes a functional programming style in R, encouraging users to write small, composable functions instead of long procedural scripts — bringing software engineering practices to the data-science community.

Despite these decades of advocacy, R scripts in academic and industry settings still routinely contain dozens of copy-pasted code blocks differentiated only by a variable name or file path. The central question this lesson addresses is straightforward yet powerful: How do we identify repeated patterns in R code and systematically extract them into well-named, parameterized functions?

SECTION 2

Core Principles of Function Extraction

Before writing a single line of refactored code, it is important to internalize the guiding principles that justify the effort. Extracting duplicated code into functions is not merely an aesthetic preference; it is grounded in well-established software engineering axioms that reduce defect rates, lower maintenance costs, and improve team collaboration. The following foundational ideas serve as the intellectual scaffolding for every refactoring decision you will make in R.

1

DRY — Don't Repeat Yourself

Every piece of knowledge should have a single, unambiguous, authoritative representation in a system. Duplicated logic means duplicated bugs and divergent behavior over time.
2

Single Responsibility

Each function should do one thing and do it well. A function that normalizes a column should not also plot a histogram — those are two separate responsibilities.
3

Abstraction via Parameterization

Identify the varying parts across duplicated blocks and promote them to function parameters. The invariant logic becomes the function body.
4

Behavior Preservation

Refactoring must not alter observable outputs. Before extracting, capture expected results; after extraction, verify identical outputs — tests are your safety net.
5

Naming as Documentation

A well-named function communicates intent better than a comment ever could. The name normalize_column() is self-documenting in a way that five lines of arithmetic are not.
✦

KEY TAKEAWAY

Think of a function as a recipe card in a professional kitchen. Without the card, every chef memorizes (or copies) the steps independently — and subtle variations creep in. With a single authoritative recipe, the head chef updates the card once and every dish is consistent. In R, the recipe card is a function: the ingredients are parameters, the steps are the body, and the finished dish is the return value. When the recipe changes, you update one function — not twenty scattered code blocks.

SECTION 3

Visual Explanation — Before and After Extraction

The following diagram contrasts a typical R script containing three duplicated blocks of normalization logic (left) with the refactored version that extracts those blocks into a single reusable function (right). Notice how the duplicated code — shaded in red — collapses into a single function definition, and each call site is replaced by a concise one-liner. The arrows trace the data flow from raw input through the parameterized function to the normalized output.

Before vs. After: Extracting Repeated Code into a FunctionBEFORE — Duplicated Blocksmean_a <- mean(df$col_a, na.rm=TRUE)sd_a <- sd(df$col_a, na.rm=TRUE)df$col_a_norm <- (df$col_a - mean_a) / sd_aBlock 1mean_b <- mean(df$col_b, na.rm=TRUE)sd_b <- sd(df$col_b, na.rm=TRUE)df$col_b_norm <- (df$col_b - mean_b) / sd_bBlock 2mean_c <- mean(df$col_c, na.rm=TRUE)sd_c <- sd(df$col_c, na.rm=TRUE)df$col_c_norm <- (df$col_c - mean_c) / sd_cBlock 312 lines · 3 bugs to fix if logic changes⚠ Maintenance risk: HIGHAFTER — Extracted Functionnormalize <- function(x) { m <- mean(x, na.rm = TRUE) s <- sd(x, na.rm = TRUE) (x - m) / s}Define oncedf$col_a_norm <- normalize(df$col_a)df$col_b_norm <- normalize(df$col_b)df$col_c_norm <- normalize(df$col_c)Call 3×7 lines · 1 place to fix if logic changes✓ Maintenance risk: LOWSame output, less code, one source of truth
Left: three nearly identical code blocks, each normalizing a different column. Right: a single normalize() function defined once and called three times. The extraction reduces lines of code and, more critically, ensures any fix to the normalization logic propagates everywhere automatically.

The diagram makes visible a pattern that is easy to overlook when scrolling through a long script: the invariant logic (computing mean, standard deviation, and the z-score) is identical across all three blocks, while the only variant element is the input vector. The extraction step promotes that variant to a parameter (x) and wraps the invariant logic in a named function. This simple mechanical transformation is the essence of the Extract Function refactoring.

SECTION 4

The Mechanics of Function Extraction in R

R's status as a functional programming language makes function extraction particularly natural. Functions in R are first-class objects: they can be assigned to variables, passed as arguments to other functions, and returned from functions. The basic syntax for defining a function is function_name <- function(param1, param2, ...) { body }. The body can contain any valid R expressions, and the last evaluated expression is implicitly returned (though an explicit return() call is also acceptable). Understanding the mechanics of scoping, default arguments, and side-effect management is essential to performing safe extractions.

Step-by-Step Extraction Protocol

  1. Identify the duplication. Scan for blocks that share the same structural pattern — same operations in the same order, differing only in one or two values.
  2. Catalog the variant parts. List every element that changes across copies. These become function parameters.
  3. Write the function signature. Choose a descriptive verb-noun name (e.g., compute_z_scores) and declare parameters with sensible defaults where appropriate.
  4. Move the invariant logic into the function body. Replace the hardcoded varying values with the parameter names.
  5. Replace each original block with a function call. Pass the original varying values as arguments.
  6. Verify behavior preservation. Compare outputs before and after using all.equal() or a testing framework such as testthat.

Scoping & Side Effects

R uses lexical scoping: a function looks up free variables in the environment where it was defined, not where it is called. This means that if your duplicated block references a global variable without passing it as a parameter, the extracted function will still find it — but this creates a hidden dependency that makes the function fragile and harder to test in isolation. Best practice is to make every input explicit through parameters and to avoid modifying objects outside the function's own environment (i.e., avoid the <<- operator unless absolutely necessary). A pure function — one that depends only on its inputs and produces no side effects — is the easiest to reason about, test, and reuse.

💡

Default Arguments as Documentation

When extracting a function, consider which parameters deserve default values. For instance, normalize <- function(x, na.rm = TRUE) communicates that missing-value removal is the expected norm, while still allowing callers to override it. Defaults reduce cognitive load at call sites and serve as inline documentation of the function's intended usage.

SECTION 5

Common Duplication Patterns in R

Not all duplication looks the same. In R programming — especially in data analysis and statistical modeling — repeated code manifests in several recurring patterns. Recognizing these patterns quickly is a skill that separates novice scripts from production-quality code. The following diagram classifies the four most common duplication patterns encountered in R, along with the corresponding extraction strategy for each.

Four Common Duplication Patterns in R1. Column-wise Repetitionmean(df$age); mean(df$income)sd(df$age); sd(df$income)→ Extract function(col)summarize_col <- function(x) { c(mean=mean(x), sd=sd(x))}Use with lapply(df, summarize_col)2. File-processing Repetitiond1 <- read.csv("jan.csv")d1 <- d1[d1$status=="A",]# ...same for feb.csv, mar.csv→ Extract function(filepath)load_and_filter <- function(fp) { d <- read.csv(fp) d[d$status == "A", ]}3. Plot-generation Repetitionggplot(df, aes(x=age)) + geom_histogram(bins=30) + ...# same theme/labels for income, etc→ Extract function(df, col_name)make_hist <- function(df, col) { ggplot(df, aes(x=.data[[col]]))+ geom_histogram(bins=30)+theme_m()}4. Model-fitting Repetitionm1 <- lm(y ~ x1, data=train)summary(m1); plot(m1)# same pattern for y~x2, y~x3 ...→ Extract function(formula)fit_and_report <- function(fml, d){ mod <- lm(fml, data = d) list(summary=summary(mod), plots=plot(mod))
Four archetypal duplication patterns in R data-analysis scripts. Each pattern shows the duplicated code at top and the corresponding extracted function below, with the parameter that captures the varying element highlighted in the function name.

Pattern 1 (column-wise repetition) is the most frequent offender in exploratory data analysis: the same summary statistics computed for every numeric column. Pattern 2 (file-processing repetition) arises when multiple CSV or Excel files must be ingested with the same cleaning steps. Pattern 3 (plot-generation repetition) appears when producing a suite of identically formatted visualizations for different variables. Pattern 4 (model-fitting repetition) occurs when fitting the same class of model across multiple predictor sets and summarizing results. In each case, the extraction follows the same logic: identify the variant, parameterize it, and wrap the invariant in a function.

SECTION 6

Worked Example — Cleaning and Summarizing Survey Data

Suppose you receive a survey dataset with three Likert-scale columns (q1, q2, q3), each containing integer responses from 1 to 5 with some missing values coded as 99. Your task is to recode 99 to NA, compute the mean and standard deviation, and report them. The original script does this three times with copy-pasted blocks.

Extracting a Survey-Column Cleaner

  1. 1

    Step 1 — Identify the Duplication

    The original script contains three blocks of the form: survey$q1[survey$q1 == 99] <- NA; mean_q1 <- mean(survey$q1, na.rm = TRUE); sd_q1 <- sd(survey$q1, na.rm = TRUE). The only element that varies across the three blocks is the column name.
  2. 2

    Step 2 — Catalog the Variant

    The variant is the column vector itself. We will parameterize it as x. The sentinel value (99) could also be parameterized for generality, so we add a sentinel parameter with a default of 99.
    Parameters: x (column vector), sentinel = 99
  3. 3

    Step 3 — Write the Extracted Function

    We define: clean_and_summarize <- function(x, sentinel = 99) { x[x == sentinel] <- NA; list(mean = mean(x, na.rm = TRUE), sd = sd(x, na.rm = TRUE), n_valid = sum(!is.na(x))) }. The function returns a named list so we can easily access each statistic. Returning a list rather than printing ensures composability.
    Function defined: clean_and_summarize()
  4. 4

    Step 4 — Replace the Original Blocks

    Each three-line block is replaced with a single call: results_q1 <- clean_and_summarize(survey$q1), and similarly for q2 and q3. To process all three at once, we can use lapply(survey[, c("q1","q2","q3")], clean_and_summarize) which returns a list of results keyed by column name.
    9 lines of duplicated code → 1 function definition + 1 lapply call
  5. 5

    Step 5 — Verify Behavior Preservation

    We compare outputs: all.equal(results_q1$mean, mean_q1_old) returns TRUE. We repeat for all columns and statistics. Once verified, the original blocks can be safely deleted. For ongoing protection, wrap these checks in testthat::test_that() assertions so any future modification to clean_and_summarize() is automatically regression-tested.
    All outputs match ✓ — refactoring is complete

SECTION 7

Strengths, Limitations, and Trade-offs

Function extraction is arguably the most universally beneficial refactoring, but like any engineering practice it carries trade-offs. Over-extraction — decomposing code into too many tiny functions — can fragment logic and make the overall flow harder to follow. Under-extraction leaves code bloated and inconsistent. The table below contrasts the primary strengths and limitations to help you calibrate how aggressively to apply this technique.

DimensionStrengthLimitation / Risk
ReadabilityDescriptive function names replace opaque code blocks, making the script read like pseudocode.Too many small functions force the reader to jump between definitions to understand the flow ("yo-yo" problem).
MaintainabilityA bug fix or enhancement is applied in one place and propagates everywhere.Changing a function's interface (adding/removing parameters) can break all call sites.
TestabilityIsolated functions can be unit-tested with known inputs and expected outputs.Functions with side effects (file I/O, global state) remain difficult to test and may require mocking.
PerformanceNegligible overhead; R function-call cost is trivial compared to data operations.In tight inner loops (millions of iterations), the overhead of function dispatch can accumulate — consider vectorization instead.
ReusabilityFunctions can be sourced across scripts or packaged into an R package for team-wide use.Premature generalization (adding parameters "just in case") increases complexity without immediate benefit.

Strengths and limitations of extracting repeated code into functions

✦

KEY TAKEAWAY

A useful heuristic is the Rule of Three: if you find yourself writing the same logic a third time, stop and extract a function. Two copies might be coincidental; three copies constitute a pattern. This heuristic balances the cost of premature abstraction against the growing risk of inconsistent duplication, much like an engineer who tolerates a manual weld once, accepts it twice, but designs a jig for the third.

SECTION 8

Connection to Advanced Functional Patterns

Extracting repeated code into named functions is the entry point to a much richer landscape of functional programming techniques available in R. Once you are comfortable writing simple extracted functions, the next step is to leverage R's higher-order functions — functions that accept other functions as arguments — to further eliminate repetitive patterns. The apply family (lapply, sapply, vapply) and the purrr package's map family provide the machinery to apply your extracted function across collections without writing a single for loop.

ConceptBasic Extraction (This Lesson)Advanced Functional Pattern
Applying to multiple inputsCall the function once per input manuallyUse lapply() or purrr::map() to iterate declaratively
Composing transformationsChain function calls sequentially with intermediate variablesUse the pipe (|> or %>%) to compose a readable pipeline
Reusing across projectsCopy the function definition to another script via source()Package functions into an R package with documentation, tests, and versioning
Customizing behaviorAdd parameters with default valuesWrite function factories (closures) that return specialized functions

Progression from basic extraction to advanced functional programming in R

The journey from duplicated code to a well-structured R package follows a clear trajectory: first extract into functions, then iterate with lapply or purrr::map, then compose with pipes, and finally bundle into a package. Each step builds directly on the foundation of function extraction covered in this lesson. Understanding closures — functions that capture and retain their enclosing environment — opens the door to function factories, memoization, and other powerful patterns that make R code both concise and expressive.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain in your own words why copy-pasting a block of R code and changing a variable name is considered a form of technical debt. What specific risks does this practice create compared to calling a parameterized function?
Show answer
Copy-pasting creates multiple representations of the same logical operation, violating the DRY principle. If a bug is discovered in the logic (e.g., the original block forgot to handle NA values), the fix must be applied independently to every copy — and missing even one copy introduces inconsistent behavior. Over time, individual copies tend to drift as developers make small, uncoordinated edits, making the codebase harder to understand and audit. A parameterized function centralizes the logic: a single fix propagates to all call sites, and the function name itself serves as documentation of intent. The risk is compounded in collaborative projects where different team members may modify different copies unaware of each other's changes.

Question 2: BASIC CALCULATION

Given the following duplicated R code, write a single extracted function and show how to call it for both cases. # Block A min_a <- min(df$temperature, na.rm = TRUE) max_a <- max(df$temperature, na.rm = TRUE) df$temp_scaled <- (df$temperature - min_a) / (max_a - min_a) # Block B min_b <- min(df$humidity, na.rm = TRUE) max_b <- max(df$humidity, na.rm = TRUE) df$hum_scaled <- (df$humidity - min_b) / (max_b - min_b)
Show answer
The invariant logic is min-max scaling; the variant is the input vector. The extracted function is: min_max_scale <- function(x) { rng <- range(x, na.rm = TRUE); (x - rng[1]) / (rng[2] - rng[1]) }. Call sites become: df$temp_scaled <- min_max_scale(df$temperature) and df$hum_scaled <- min_max_scale(df$humidity). Notice the use of range() to compute min and max in a single call, which is both more concise and marginally more efficient than two separate calls. The function is pure — it depends only on its input and produces no side effects — making it trivially testable.

Question 3: INTERMEDIATE

A colleague's R script reads three CSV files, filters each to rows where year >= 2020, converts a date column from character to Date class, and writes the result to a new CSV. The logic is identical for all three files, differing only in the file paths. Write an extracted function with appropriate parameters and default values, then show how to apply it to all three files using lapply().
Show answer
The function should parameterize the input path, output path, the minimum year (with a default), and the date column name (with a default). For example: process_file <- function(input_path, output_path, min_year = 2020, date_col = "date") { d <- read.csv(input_path, stringsAsFactors = FALSE); d <- d[d$year >= min_year, ]; d[[date_col]] <- as.Date(d[[date_col]]); write.csv(d, output_path, row.names = FALSE); invisible(d) }. To apply across files: files_in <- c("data/q1.csv", "data/q2.csv", "data/q3.csv"); files_out <- gsub("data/", "clean/", files_in); results <- Map(process_file, files_in, files_out). We use Map() (the multivariate version of mapply) because we have two parallel lists of arguments. The invisible(d) allows optional capture of the cleaned data without cluttering the console.

Question 4: APPLIED

You are building a report that fits a linear regression for each of five predictor variables against a common response y in a data frame df. For each model, you extract R², the coefficient estimate, and its p-value. Write a function fit_univariate() that accepts a predictor column name, fits the model, and returns these three summaries in a named list. Then demonstrate how to apply it across all five predictors and combine the results into a single data frame.
Show answer
The function: fit_univariate <- function(predictor, data, response = "y") { fml <- as.formula(paste(response, "~", predictor)); mod <- lm(fml, data = data); s <- summary(mod); coeff <- s$coefficients[2, ]; list(predictor = predictor, r_squared = s$r.squared, estimate = coeff["Estimate"], p_value = coeff["Pr(>|t|)"]) }. Application: predictors <- c("x1","x2","x3","x4","x5"); results <- lapply(predictors, fit_univariate, data = df); results_df <- do.call(rbind, lapply(results, as.data.frame)). The key design decision is accepting the predictor as a string and constructing the formula with as.formula(paste(...)), which allows programmatic iteration. The response variable has a default of "y" but can be overridden, making the function reusable for different datasets.

Question 5: CRITICAL THINKING

Consider a scenario where two code blocks are structurally similar but not identical: Block A computes the median of a column and then applies a log transformation, while Block B computes the mean and applies a square-root transformation. A colleague proposes extracting a single function with parameters center_fn and transform_fn that accept function objects. Discuss: (a) Under what circumstances is this a good design? (b) When might this over-generalization harm readability? (c) What R language feature makes this possible, and what concept from functional programming does it exemplify?
Show answer
(a) This design is beneficial when the overall computational pipeline is genuinely shared — e.g., both blocks read a column, apply a centering operation, then apply a transformation, then store the result. If additional variants are expected (e.g., trimmed mean with Box-Cox transform), the higher-order function avoids proliferating near-identical functions. (b) Over-generalization harms readability when there are only two cases and they are unlikely to grow; the function's signature becomes an abstract puzzle, and callers must understand that they are passing functions as arguments — a cognitive leap for less experienced R users. The Rule of Three suggests waiting for a third variant before generalizing this far. (c) R's support for first-class functions makes this possible: functions can be stored in variables and passed as arguments. This exemplifies the strategy pattern from object-oriented design, or more precisely, the concept of higher-order functions from functional programming. The ability to pass median or mean as arguments relies on R treating them as objects no different from numeric vectors or character strings.

SUMMARY

Lesson Summary

Summary

Refactoring repeated code into functions is the most fundamental transformation in the R programmer's toolkit. The process begins with identifying duplicated blocks — code segments that share the same structure but differ in one or more values. Those varying values become function parameters, while the shared logic becomes the function body. A descriptive verb-noun function name serves as self-documentation, and default argument values encode the most common usage pattern.

The guiding principles — DRY, single responsibility, and behavior preservation — ensure that refactoring improves structure without introducing regressions. After extraction, verify correctness with all.equal() or testthat. The Rule of Three provides a practical threshold for when to extract: tolerate one copy, note two, and refactor at three. This foundational skill paves the way toward higher-order functions, functional pipelines, and ultimately R package development.

Varsity Tutors • R Programming • Refactoring — Refactor repeated code into functions