CTCaliberTek
Course: R Programming Path: From Beginner to Reproducible Reports · Lesson 4 of 8Log in to track your progress and earn a certificate when you finish the course.
Programming

R Functions, Control Flow, and the apply Family

Published November 9, 2026·Updated November 9, 2026·10 min read

Edited by , Senior Data Analyst. How we make our content

Like SaveLog in to like, save, and track progress.

Introduction

Once you repeat the same calculation more than twice, it should become a function. R is a functional language, so functions are ordinary objects that you can store, pass to other functions, and return from them. This lesson covers defining functions, branching and looping, why vectorized code is faster and clearer than loops, and the apply family that replaces most loops.

Prerequisites

  • Vectors and data frames from the previous lessons
  • R 4.1 or newer for the native pipe and the short function syntax

Step-by-Step Guide

Step 1: Define a function

A function is created with the function keyword, a list of arguments, and a body in curly braces. The value of the last expression in the body is returned automatically, so explicit return calls are only needed for early exits. Arguments can have default values, and callers can pass them by position or by name. Naming arguments in calls makes code self-documenting, especially when a function has several options.

bmi <- function(weight_kg, height_m, digits = 1) {
  round(weight_kg / height_m^2, digits)
}
bmi(70, 1.75)               # 22.9
bmi(70, 1.75, digits = 2)   # 22.86
bmi(height_m = 1.6, weight_kg = 55)

Step 2: Make functions defensive

A function that silently accepts bad input produces wrong answers that are hard to trace. Use stopifnot to assert conditions at the top of the function, or stop with a clear message. Because R vectorizes, a function written with arithmetic and no loops will often work on whole columns without changes, which is one of the most useful properties to aim for. Keep functions small and focused on one task, and give them verb-like names.

percent_change <- function(old, new) {
  stopifnot(is.numeric(old), is.numeric(new), all(old != 0))
  (new - old) / old * 100
}
percent_change(c(100, 200), c(110, 180))   # 10 -10

Step 3: Branch with if, else, and ifelse

The if statement takes a single TRUE or FALSE, so it works on one value at a time, and using it with a longer vector is an error in current R versions. To make a decision for every element of a vector, use ifelse, or case_when from dplyr when there are several categories. This distinction between scalar branching and vectorized branching trips up many beginners. Combine conditions with && and || for single values, and with & and | for vectors.

score <- 72
if (score >= 90) {
  grade <- "A"
} else if (score >= 70) {
  grade <- "B"
} else {
  grade <- "C"
}

scores <- c(95, 72, 58, 88)
ifelse(scores >= 70, "pass", "fail")   # "pass" "pass" "fail" "pass"

Step 4: Use loops when they are the clearest choice

A for loop repeats a block for each item in a sequence. Prefer seq_along over 1:length(x) because the second form misbehaves when x is empty, producing the sequence 1, 0. Loops are fine for tasks that are inherently sequential, such as reading many files in turn, but for arithmetic on whole vectors a vectorized expression is shorter and faster. Growing a vector inside a loop by appending is slow for large data, so preallocate the result when you must loop.

files <- c("jan.csv", "feb.csv", "mar.csv")
for (i in seq_along(files)) {
  cat("Reading file", i, "of", length(files), ":", files[i], "\n")
}

# vectorized: no loop needed
prices <- c(10, 20, 30)
prices * 1.08            # add 8% tax to every price

Step 5: Replace loops with lapply, sapply, and vapply

The apply family applies a function to each element of a vector or list. lapply always returns a list. sapply tries to simplify the result into a vector or matrix, which is convenient at the console but can return different shapes depending on the input. vapply is the safe version: you declare the type and length of each result, and R stops with an error if a result does not match, which makes code in functions and packages predictable. You can write the function inline using the short lambda syntax with a backslash, available since R 4.1.

temps_c <- c(0, 37, 100)
sapply(temps_c, \(x) x * 9 / 5 + 32)            # 32 98.6 212
vapply(temps_c, \(x) x * 9 / 5 + 32, numeric(1))

parts <- lapply(1:3, \(n) rep("x", n))   # a list of three vectors
sapply(mtcars, mean)                      # mean of every column of a data frame

Step 6: Chain steps with the native pipe

The pipe operator, written as a vertical bar followed by a greater-than sign, passes the result on its left as the first argument of the function on its right. It turns nested calls that read inside out into a sequence that reads left to right, top to bottom. It has been part of base R since version 4.1, and the tidyverse packages are designed around it. Use it for steps that transform one object, and give the intermediate result a name whenever a chain grows beyond a handful of steps.

mtcars |>
  subset(cyl == 4) |>
  nrow()                       # 11 four-cylinder cars

c(4, 9, 16) |> sqrt() |> sum() # 9

FAQs

Do R functions need an explicit return()?
No. A function returns the value of its last expression. Use return() only for early exits, such as returning NA when the input is empty, because calling it everywhere adds noise and slightly slows the function.
Are loops slow in R?
Modern R loops are acceptable, and the common slowness comes from growing objects inside the loop or doing work that vectorized code does in one step. Write the clear version first, and optimize only after measuring.
When should I use sapply versus vapply?
sapply is handy for interactive exploration. Inside functions and scripts that other people run, prefer vapply, because it checks the type and length of every result and fails loudly instead of returning a surprising shape.

Summary

You can now package logic into functions with defaults and checks, branch with if and ifelse, loop with seq_along, apply functions across data with lapply, sapply, and vapply, and chain steps with the native pipe. These tools make your analysis shorter and easier to test. In the next lesson you will use the dplyr package, which builds on these ideas to make data wrangling much more readable.

Related tutorials

Comments

Log in or create a free account to join the discussion.

  • No comments yet. Be the first to comment.