Lesson 17 of 17 · Week 09 · Thu Nov 19 · Tue Nov 24
Writing Good Software
Questions
- How can I write code that is easy to read, review and trust?
Objectives
- Structure a script into inputs, processing and outputs, with comments that explain why.
- Replace repeated code with functions.
- Choose descriptive names and follow a consistent style.
- Write tests with
stopifnot()that fail on buggy code.
Write code for people
Code is read many more times than it is written: by classmates reviewing it, by your instructor, and by you in three months. A computer runs messy code just as happily as clean code; people can't. This week is about habits that make your code easy to read, check and change.
Adapted from Software Carpentry's “Writing Good Software” episode (CC BY 4.0, The Carpentries). Text, examples and exercises have been rewritten for this course.
Structure a script
# clean-survey.R
# Reads the raw survey, fixes known data-entry problems, writes results/survey_clean.csv.
# Author: Ana Rojas, 2026-11-19
library(dplyr) # 1. packages at the top
raw <- read.csv("data/survey.csv") # 2. read inputs (relative paths)
# 3. clean -------------------------------------------------------------
clean <- raw |>
mutate(age = if_else(age == -99, NA, age)) # -99 means "not answered"
# 4. write outputs -----------------------------------------------------
write.csv(clean, "results/survey_clean.csv", row.names = FALSE)
A header saying what the script does, packages first, then input → processing → output. Comments explain why (“-99 means not answered”), not what. The code already says what. In Positron, a comment ending in four or more dashes becomes a section you can jump to from the outline.
Don't repeat yourself
Copy-pasted code is where bugs hide: you fix one copy and forget the others, or change a name in one place only. When you
see the same lines twice, write a function (Week 05) and call it twice. Put numbers that mean something in named objects
(kmh_per_knot <- 1.852) instead of repeating “magic numbers”.
Names and style
Follow one style consistently. This course uses the tidyverse style guide:
- Names in
lower_snake_casethat say what the thing is:life_exp_2007, notx2ordf1. Functions are verbs:standardize(). <-for assignment; spaces around operators (x <- a + b) and after commas (f(x, y)); no space before the parenthesis in a function call.- Lines under 80 characters; break long pipes after
|>, one step per line. TRUE/FALSE, neverT/F(those can be overwritten).
Positron can reformat a file for you (right-click → Format Document). It fixes spacing, but it can't choose good names.
Test your assumptions
stopifnot(nrow(clean) == nrow(raw)) # cleaning shouldn't drop rows
stopifnot(all(clean$age >= 0, na.rm = TRUE))
# a tiny test for a function: known inputs, known answers
stopifnot(isTRUE(all.equal(standardize(c(1, 2, 3)), c(-1, 0, 1))))
A test is code that stops loudly if something you believe is false. Put checks after each important step, and write a
few tests with known answers for every function. Good tests fail on wrong code; a test that can't fail isn't
testing anything. For larger projects, the testthat package organises tests.
Version control and reproducibility
- Git keeps the history of your project, so you can see what changed and go back. Positron's Source Control view (in the Activity Bar) lets you commit without the command line.
- Record your setup with
sessionInfo()in reports, so others know which versions you used. - Before you share: restart R, run your scripts from the top, and check that everything works. If it only works in your current session, it doesn't work.
Exercises
Each exercise gives you up to 10 checked attempts. Use Run as many times as you like to experiment first, then click Check answer when you're ready. Your exercises use numbers/subsets unique to you, so answers won't match a classmate's exactly.
Exercise 1 — Stop repeating yourself
0 / 10 attempts usedClick Run to see copy-pasted code that standardizes three columns of gm (subtract the mean,
divide by the standard deviation). One copy has a bug. Replace it with a function standardize(x), and use it to
create z_life, z_gdp and z_pop (for lifeExp, gdpPercap
and pop). Did you spot the bug?
Exercise 2 — Names that say what they mean
0 / 10 attempts usedsurvey came from a spreadsheet with awkward column names (click Run). Give it new column
names with names(survey) <- c(...). Each new name must be:
lower_snake_case: lowercase letters, digits and single underscores, starting with a letter;- descriptive: at least 3 characters, and not a vague word like
x,var,data,colortemp; - unique.
Keep units where they matter (e.g. height_cm).
Exercise 3 — Tests that catch bugs
0 / 10 attempts usedpct_change(old, new) should return the percentage change from old to new, e.g.
pct_change(50, 60) is 20 and pct_change(80, 60) is -25. Write a function
test_pct_change(f) that tests any candidate function f with stopifnot(), using inputs
whose answers you know. It must run silently for a correct f and stop with an error for
buggy ones. I'll try it on the correct function and on three different buggy versions.
Exercise 4 — Make it readable
0 / 10 attempts usedClick Run to see a working but badly styled snippet. Rewrite it inside the raw string as
tidy_code so that it follows the style rules above: <- for assignment, spaces around
<-, =, +, *, /, > and after commas,
TRUE not T, lines under 80 characters, and a better name than X for the result. It must
still compute the same values.
Key Points
- Write code for people: a header, clear sections, comments that say why.
- If you've written it twice, write a function.
- Descriptive
lower_snake_casenames and consistent spacing make code easy to review. - A good test fails on wrong code; check your assumptions with
stopifnot(). - Restart R and run from the top before sharing.
Proof of completion
Once you've attempted every exercise above, download a PDF summary of your work on this lesson — your name, the exercises you passed, attempt counts, timing, and your final code for each.