R Advanced Topics — OOP, Functional Programming, and Best Practices
Learning Objectives
By the end of this tutorial, you will be able to:
- Implement object-oriented programming with S3, S4, and R6
- Apply functional programming concepts (closures, purrr)
- Optimize R code for performance
- Write production-quality R code
- Use modern R development tools
Object-Oriented Programming
S3 Classes (Simple)
# Create S3 class
person <- function(name, age) {
structure(
list(name = name, age = age),
class = "person"
)
}
# Method
greet <- function(x, ...) {
UseMethod("greet")
}
greet.person <- function(x, ...) {
paste("Hello, my name is", x$name, "and I am", x$age, "years old.")
}
# Create object
alice <- person("Alice", 30)
greet(alice)
# [1] "Hello, my name is Alice and I am 30 years old."
# Check class
class(alice) # [1] "person"
inherits(alice, "person") # [1] TRUE
S4 Classes (Formal)
# Define S4 class
setClass("Person",
representation(
name = "character",
age = "numeric"
)
)
# Constructor
setMethod("initialize", "Person", function(.Object, name, age) {
.Object@name <- name
.Object@age <- age
.Object
})
# Method
setGeneric("greet", function(object) standardGeneric("greet"))
setMethod("greet", "Person", function(object) {
paste("Hello, I am", object@name)
})
# Create object
alice <- new("Person", name = "Alice", age = 30)
greet(alice)
R6 Classes (Reference Classes)
library(R6)
Person <- R6Class("Person",
public = list(
name = NULL,
age = NULL,
initialize = function(name, age) {
self$name <- name
self$age <- age
},
greet = function() {
paste("Hello, I am", self$name)
},
birthday = function() {
self$age <- self$age + 1
invisible(self)
}
)
)
# Create object
alice <- Person$new("Alice", 30)
alice$greet() # [1] "Hello, I am Alice"
alice$birthday()
alice$age # [1] 31
S3 vs S4 vs R6
| Feature | S3 | S4 | R6 |
|---|---|---|---|
| Complexity | Simple | Formal | Reference |
| Encapsulation | No | Partial | Yes |
| Method dispatch | Generic | Generic | Method |
| Inheritance | Manual | Built-in | Built-in |
| Performance | Fastest | Slower | Fast |
| Use case | Quick classes | Bioconductor | Production |
Functional Programming
First-Class Functions
# Functions are objects
square <- function(x) x^2
class(square) # [1] "function"
# Assign to variable
f <- square
f(5) # [1] 25
# Pass as argument
apply_function <- function(f, x) f(x)
apply_function(square, 5) # [1] 25
# Return function
make_adder <- function(n) {
function(x) x + n
}
add5 <- make_adder(5)
add5(10) # [1] 15
Closures
# Functions that capture their environment
counter <- function() {
count <- 0
list(
increment = function() {
count <<- count + 1
count
},
get = function() count
)
}
c <- counter()
c$increment() # [1] 1
c$increment() # [1] 2
c$get() # [1] 2
Purrr (Functional Programming)
library(purrr)
# Map variants
map(1:5, \(x) x^2) # Returns list
map_dbl(1:5, \(x) x^2) # Returns double vector
map_chr(1:5, \(x) paste(x)) # Returns character vector
map_lgl(1:5, \(x) x > 3) # Returns logical vector
# Map2 — two inputs
map2(c(1, 2, 3), c(10, 20, 30), \(x, y) x + y)
# Pmap — multiple inputs
pmap(list(1:3, 4:6, 7:9), \(x, y, z) x + y + z)
# Keep and discard
keep(1:10, \(x) x > 5) # [1] 6 7 8 9 10
discard(1:10, \(x) x > 5) # [1] 1 2 3 4 5
# Reduce
reduce(1:5, `+`) # [1] 15
# Accumulate
accumulate(1:5, `+`)
# [1] 1 3 6 10 15
# Compose
compose <- function(f, g) function(...) f(g(...))
add1_square <- compose(\(x) x^2, \(x) x + 1)
add1_square(5) # [1] 36
Performance Optimization
Vectorization
# Slow: loop
x <- 1:1000000
result <- numeric(length(x))
for (i in seq_along(x)) {
result[i] <- x[i]^2
}
# Fast: vectorized
result <- x^2
# Fast: apply
result <- sapply(x, \(x) x^2)
Data.table
library(data.table)
# Convert to data.table
dt <- as.data.table(mtcars)
# Fast operations
dt[, .(mean_mpg = mean(mpg), sd_mpg = sd(mpg)), by = cyl]
# Fast reading
dt <- fread("large_file.csv")
# Fast writing
fwrite(dt, "output.csv")
Parallel Processing
library(parallel)
# Detect cores
n_cores <- detectCores()
# Make cluster
cl <- makeCluster(n_cores - 1)
# Parallel apply
result <- parLapply(cl, 1:1000, function(x) x^2)
# Stop cluster
stopCluster(cl)
# Using foreach
library(foreach)
library(doParallel)
registerDoParallel(cl)
result <- foreach(i = 1:1000, .combine = c) %dopar% {
i^2
}
stopCluster(cl)
Memory Optimization
# Check object size
object.size(mtcars)
# Remove objects
rm(x, y, z)
# Clear all
rm(list = ls())
# Use data.table for large datasets
# Use fst for fast serialization
Best Practices
Code Style
# Use snake_case for variables and functions
my_variable <- 10
my_function <- function(x) x^2
# Use UPPER_CASE for constants
MAX_RETRIES <- 3
PI <- 3.14159
# Use informative names
calculate_average <- function(scores) {
mean(scores, na.rm = TRUE)
}
# Not: calc_avg <- function(s) mean(s, na.rm = TRUE)
# Add spaces around operators
x <- 1 + 2
y <- x * 3
# Not: x<-1+2
Documentation
#' Calculate BMI
#'
#' @param weight_kg Weight in kilograms
#' @param height_cm Height in centimeters
#' @return BMI value (numeric)
#' @examples
#' calculate_bmi(70, 175)
calculate_bmi <- function(weight_kg, height_cm) {
height_m <- height_cm / 100
weight_kg / (height_m ^ 2)
}
Error Handling
safe_function <- function(x) {
if (!is.numeric(x)) {
stop("x must be numeric", call. = FALSE)
}
if (length(x) == 0) {
stop("x must not be empty", call. = FALSE)
}
mean(x, na.rm = TRUE)
}
Modern R Features
Native Pipe (R 4.1+)
# Base pipe
1:10 |>
mean() |>
round(2)
# With placeholder (R 4.2+)
"hello world" |>
toupper() |>
paste("!", sep = "")
Lambda Functions (R 4.1+)
# Anonymous functions
\(x) x^2
# In apply
sapply(1:5, \(x) x^2)
# In pipes
1:5 |>
sapply(\(x) x^2) |>
sum()
Wildcard Bindings (R 4.0+)
# Tilde imports
library(tidyverse)
# Or selective imports
library(dplyr, include.only = c("filter", "select", "mutate"))
Practical Examples
Example 1: Custom Report Generator
generate_report <- function(data, output_file) {
# Create Rmd content
rmd_content <- paste0(
'---
title: "Data Report"
output: html_document
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = FALSE)
Summary
summary(', deparse(substitute(data)), ')
Plot
plot(', deparse(substitute(data)), '$x, ', deparse(substitute(data)), '$y)
' )
writeLines(rmd_content, output_file) rmarkdown::render(output_file) }
### Example 2: Validation Package
```r
# validators.R
check_numeric <- function(x, name = deparse(substitute(x))) {
if (!is.numeric(x)) stop(name, " must be numeric")
if (length(x) == 0) stop(name, " must not be empty")
if (any(is.na(x))) warning(name, " contains NA values")
invisible(TRUE)
}
check_positive <- function(x, name = deparse(substitute(x))) {
check_numeric(x, name)
if (any(x < 0)) stop(name, " must be positive")
invisible(TRUE)
}
# Usage
x <- c(1, 2, -3, 4)
check_numeric(x)
# Warning: x contains NA values
check_positive(x)
# Error: x must be positive
Practice Exercises
Exercise 1: R6 Class System
Create an R6 class BankAccount with deposit, withdraw, and balance methods. Include error handling for insufficient funds.
Solution
library(R6)
BankAccount <- R6Class("BankAccount",
public = list(
balance = 0,
initialize = function(initial_balance = 0) {
if (initial_balance < 0) stop("Initial balance must be positive")
self$balance <- initial_balance
},
deposit = function(amount) {
if (amount <= 0) stop("Deposit must be positive")
self$balance <- self$balance + amount
invisible(self)
},
withdraw = function(amount) {
if (amount <= 0) stop("Withdrawal must be positive")
if (amount > self$balance) stop("Insufficient funds")
self$balance <- self$balance - amount
invisible(self)
},
get_balance = function() {
self$balance
}
)
)
# Usage
account <- BankAccount$new(1000)
account$deposit(500)$withdraw(200)$get_balance()
# [1] 1300
Key Takeaways
- S3 is simplest — use for everyday classes
- S4 is formal — use for Bioconductor packages
- R6 is reference-based — use for stateful objects
- Functional programming — closures, map, reduce, compose
- Vectorize before looping — 10-100x speedups
data.tableis fast — for large datasets- Parallel processing —
parallelandforeachpackages - Always document functions — roxygen2 format
- Handle errors gracefully —
stop(),warning(),tryCatch() - Use modern R features — pipe
|>, lambda\(x)
Congratulations!
You have completed the R Programming A-Z course! You now have comprehensive knowledge from basics to advanced topics. Keep practicing, explore packages, and build real projects to solidify your skills.