Skip to content

Functions

Functions let you wrap a block of code into a single reusable command. In bioinformatics, you often repeat the same filtering, normalization, or annotation step across many datasets. Writing a function means you define the logic once and call it whenever you need it. This reduces errors and makes your code easier to read.

A function in R has three parts: a name, a set of inputs, and a body of code that runs when you call it.

Here is a function that calculates the GC content of a DNA sequence. GC content is the fraction of bases that are guanine or cytosine.

#' CalculateGcContent.
#'
#' @param sequence Input value.
#' @return Computed result.
CalculateGcContent <- function(sequence) {
bases <- strsplit(toupper(sequence), "")[[1]]
gc_count <- sum(bases %in% c("G", "C"))
gc_fraction <- gc_count / length(bases)
return(gc_fraction)
}
CalculateGcContent("ATGCGATCGA")
[1] 0.5

The function splits the string into individual characters. It counts how many are G or C. Then it divides by the total number of bases. Let’s try a sequence with no GC content.

CalculateGcContent("AAATTTAAATTT")
[1] 0

This returns zero because there are no G or C bases in the input.

You can give function arguments default values. The caller can override them when needed. This is useful when you have a standard threshold but occasionally want a different one.

The function below labels genes from a differential expression analysis. It takes a p-value and a log2 fold change and returns “up”, “down”, or “ns” for not significant.

#' LabelSignificance.
#'
#' @param pvalue Input value.
#' @param log2fc Input value.
#' @param alpha Input value.
#' @param fc_threshold Input value.
#' @return Computed result.
LabelSignificance <- function(pvalue, log2fc, alpha = 0.05, fc_threshold = 1) {
if (pvalue < alpha & log2fc > fc_threshold) {
return("up")
} else if (pvalue < alpha & log2fc < -fc_threshold) {
return("down")
} else {
return("ns")
}
}
LabelSignificance(0.001, 2.5)
[1] "up"

The p-value is below 0.05 and the fold change exceeds 1. The gene is labeled as upregulated.

LabelSignificance(0.001, -1.8)
[1] "down"

A negative fold change with a significant p-value means the gene is downregulated.

LabelSignificance(0.5, 3.0)
[1] "ns"

Even though the fold change is large, the p-value is above the threshold. The gene is not significant.

You can override defaults by naming the argument you want to change.

LabelSignificance(0.001, 0.5, fc_threshold = 0.25)
[1] "up"

Here we lowered the fold change threshold to 0.25. Now a fold change of 0.5 qualifies as upregulated.

R functions can only return a single object. To return multiple values, pack them into a list. This is common when you want to compute several summary statistics at once.

#' SummarizeCounts.
#'
#' @param counts Input value.
#' @return Computed result.
SummarizeCounts <- function(counts) {
result <- list(
total = sum(counts),
mean = mean(counts),
median = median(counts),
nonzero = sum(counts > 0),
zero_fraction = mean(counts == 0)
)
return(result)
}
gene_counts <- c(0, 15, 230, 0, 45, 120, 0, 78)
summary <- SummarizeCounts(gene_counts)
summary$total
[1] 488
summary$zero_fraction
[1] 0.375

You access each value using the $ operator with the name you assigned in the list. In this case, 37.5% of the samples had zero counts for this gene.

Scope controls where a variable is visible. Variables created inside a function are local to that function. They do not affect variables outside the function, even if they share the same name.

threshold <- 0.05
#' CheckSignificance.
#'
#' @param pvalue Input value.
#' @return Computed result.
CheckSignificance <- function(pvalue) {
threshold <- 0.01 # This is a local variable
return(pvalue < threshold)
}
CheckSignificance(0.03)
[1] FALSE

Inside the function, threshold is 0.01. A p-value of 0.03 is not below 0.01, so the function returns FALSE.

threshold # Unchanged
[1] 0.05

The global threshold is still 0.05. The function’s local variable did not overwrite it.

R copies, so a function cannot change your vector

Section titled “R copies, so a function cannot change your vector”

R goes further than most languages here. Modifying an argument inside a function copies it first, so the caller’s object is untouched even when you assign into it:

#' AddFlag.
#'
#' @param genes Input value.
#' @return Computed result.
AddFlag <- function(genes) {
genes[length(genes) + 1] <- "FLAGGED"
genes
}
my_genes <- c("TP53", "BRCA1")
invisible(AddFlag(my_genes))
my_genes
caller's vector after add_flag: TP53 BRCA1

This is the opposite of Python, where the same code would modify the caller’s list, and it is why R has far fewer aliasing bugs.

Superassignment assigns in the enclosing scope rather than locally:

counter <- 0
#' Bump.
#'
#' @return Computed result.
Bump <- function() {
counter <<- counter + 1
}
Bump(); Bump()
counter
counter after two bump() calls: 2

The quieter problem is a function that reads a global it was never given:

cutoff <- 0.05
#' IsSignificant.
#'
#' @param p Input value.
#' @return Computed result.
IsSignificant <- function(p) {
p < cutoff # silent dependency
}

This works in your session. It fails in a fresh one, in someone else’s script, or when cutoff changes three hundred lines away. The function looks self contained, but it is not.

Pass it in, with a default:

#' IsSignificant.
#'
#' @param p Input value.
#' @param cutoff Input value.
#' @return Computed result.
IsSignificant <- function(p, cutoff = 0.05) p < cutoff

Loops are common in programming. R offers the apply family of functions as a concise alternative. These functions apply a function to each element of a data structure without writing an explicit loop.

The apply function works on matrices and data frames. You choose whether to apply a function across rows or columns.

  • 1 means apply across rows.
  • 2 means apply across columns.
count_matrix <- matrix(
c(100, 250, 50, 120, 300, 75, 90, 280, 60),
nrow = 3,
byrow = TRUE,
dimnames = list(
c("TP53", "BRCA1", "EGFR"),
c("Sample1", "Sample2", "Sample3")
)
)
# Row means (per gene)
apply(count_matrix, 1, mean)
TP53 BRCA1 EGFR
133.3333 165.0000 143.3333

Each value is the average expression of that gene across all three samples.

# Column sums (per sample)
apply(count_matrix, 2, sum)
Sample1 Sample2 Sample3
310 830 185

Each value is the total expression across all genes for that sample. This is a quick way to check library sizes.

sapply applies a function to each element of a vector or list. It returns a simplified result, usually a named vector.

genes <- c("TP53", "BRCA1", "EGFR", "KRAS", "MYC")
sapply(genes, nchar)
TP53 BRCA1 EGFR KRAS MYC
4 5 4 4 3

This counts the number of characters in each gene name. The result is a named vector.

lapply always returns a list. sapply tries to simplify the result into a vector or matrix. Use lapply when you want to preserve the list structure. Use sapply when you want a cleaner output.

sequences <- c("ATGCGA", "AAATTT", "GCGCGC")
lapply(sequences, CalculateGcContent)
[[1]]
[1] 0.5
[[2]]
[1] 0
[[3]]
[1] 1

lapply returns a list with one element per sequence.

sapply(sequences, CalculateGcContent)
ATGCGA AAATTT GCGCGC
0.5 0.0 1.0

sapply collapses the result into a named numeric vector. This is often easier to work with.

Sometimes you need a quick function for a single use. You can define it inline without giving it a name. This is called an anonymous function.

counts <- list(
sample1 = c(100, 200, 0, 50),
sample2 = c(300, 0, 150, 80),
sample3 = c(50, 100, 200, 0)
)
sapply(counts, function(x) sum(x > 0))
sample1 sample2 sample3
3 3 3

This counts how many genes have nonzero expression in each sample. The anonymous function function(x) sum(x > 0) is defined right where it is used.

Function Input Output Use case
apply Matrix or data frame Vector or matrix Apply a function across rows or columns
sapply Vector or list Simplified vector or matrix Apply a function to each element with clean output
lapply Vector or list List Apply a function to each element, keep list structure
vapply Vector or list Vector with specified type Like sapply but with a guaranteed return type

You now know how to write reusable functions and apply them across data structures. In the next section, you will learn how to extend R with external packages. See Working with Packages.