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.

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?

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.

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.

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.

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.

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 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.

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
Step 1 — Identify the DuplicationThe 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
Step 2 — Catalog the VariantThe 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
Step 3 — Write the Extracted FunctionWe 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
Step 4 — Replace the Original BlocksEach 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
Step 5 — Verify Behavior PreservationWe 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

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.

Strengths and limitations of extracting repeated code into functions
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.
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.

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.

Progression from basic extraction to advanced functional programming in R
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

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.

Practice Problems

PROBLEM 1CONCEPTUAL
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?
PROBLEM 2BASIC 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)
PROBLEM 3INTERMEDIATE
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().
PROBLEM 4APPLIED
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.
PROBLEM 5CRITICAL 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?

Lesson 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