R
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Data Structures
-
Section 4: Data Manipulation
-
Section 5: Visualization and Statistics
-
Section 6: Advanced R
-
Section 7: Practical Projects
-
Section 8: Interview Practice
-
Section 9: More Practice Exercises
-
Section 10: Shiny Apps in Depth
-
Section 11: More Data Wrangling
-
Section 12: Tidyverse Deep Dive
-
Section 13: Statistical Modeling Deep Dive
-
Section 14: Machine Learning in R
-
Section 15: R Visualization Deep Dive
-
Section 16: R Package Development Deep Dive
-
Section 17: R for Reproducible Research
-
Section 18: R and Databases
-
Section 19: R Performance Optimization
-
Section 20: Bioinformatics and Specialized R
-
Section 21: More Shiny Practice
-
Section 22: More Practice Exercises
-
Section 23: R Data Structures Deep Dive
-
Section 24: More Interview and Review
-
Section 25: R for Business Analytics
-
Section 26: R Text Mining and NLP
-
Section 27: R Spatial Data Analysis
-
Section 28: R Deep Learning
-
Section 29: Advanced Statistical Techniques
-
Section 30: R Object Systems Deep Dive
-
Section 31: R Environments and Metaprogramming
-
Section 32: R for Finance
-
Section 33: R for Clinical and Health Data
-
Section 34: More Shiny Advanced Practice
-
Section 35: R Data Cleaning Deep Dive
-
Section 36: R Reporting Automation
-
Section 37: More Practical Projects Round 2
-
Section 38: R Ecosystem and Career
-
Section 39: More Visualization Practice
-
Section 40: R for Experimentation
-
Section 41: R for Genomics and Bioinformatics Deep Dive
-
Section 42: R for Marketing Analytics
-
Section 43: R Data Import/Export Deep Dive
-
Section 44: R String Processing Deep Dive
-
Section 45: R for Actuarial and Insurance Analytics
-
Section 46: R Testing and Quality Assurance Deep Dive
-
Section 47: R Version Control and Collaboration
-
Section 48: R Advanced Functional Programming
-
Section 49: R for Supply Chain and Operations
-
Section 50: More Practice Exercises Round 3
-
Section 51: R Dashboards and BI Integration
-
Section 52: R Data Governance and Ethics
-
Section 53: More Modeling Practice
-
Section 54: R Final Capstone Projects
-
Section 55: R for Sports Analytics
-
Section 56: More Interview Practice Round 2
-
Section 57: R Networking and APIs
-
Section 58: R for Environmental Science
-
Section 59: R Notebook and Documentation Practices
-
Section 60: More Data Wrangling Mastery
-
Section 61: R for A/B Testing at Scale
-
Section 62: R Package Ecosystem Deep Dive
31: Object-Oriented Programming in R (S3, S4, R6)
If you've spent any time in R, you've already been using Object-Oriented Programming (OOP), even if you didn't realize it. Every time you call print() or plot() on a data frame versus a linear model, R is doing some magic behind the scenes to figure out which version of that function to run. But when you start building your own complex systems, you'll hit a wall where standard functions aren't enough. You'll be tempted to start writing massive functions with endless if/else blocks to handle different "types" of data, and that's exactly where things start to fall apart.
The 'Giant Switch Statement' Trap
I've seen this a dozen times in production code: a developer creates a list to represent a "Project" and adds a type field to it. Then, they write a function called calculate_budget() that looks like this:
calculate_budget <- function(project) {
if (project$type == "fixed") {
return(project$total)
} else if (project$type == "hourly") {
return(project$rate * project$hours)
} else if (project$type == "retainer") {
return(project$monthly_fee * project$months)
} else {
stop("Unknown project type!")
}
}
This works fine when you have two types. It's a nightmare when you have twenty. Every time you add a new project type, you have to hunt down every single function that checks project$type and add another else if. It's brittle, it's hard to test, and it's not how R is designed to work. We want polymorphism—the ability for the object itself to determine how a function should behave.
S3: The 'Good Enough' Approach
The first tool in your kit is S3. It's the most common system in R because it's essentially "informal." You don't define a class in a rigid way; you just assign a class attribute to a list. To make a function polymorphic, you create a "generic" using UseMethod().
Instead of that giant if block, we do this:
# 1. Define the generic
calculate_budget <- function(x) {
UseMethod("calculate_budget")
}
# 2. Define the methods for specific classes
calculate_budget.fixed <- function(x) x$total
calculate_budget.hourly <- function(x) x$rate * x$hours
calculate_budget.retainer <- function(x) x$monthly_fee * x$months
# 3. Create the objects
proj1 <- list(total = 1000)
class(proj1) <- "fixed"
proj2 <- list(rate = 50, hours = 10)
class(proj2) <- "hourly"
calculate_budget(proj1) # Returns 1000
calculate_budget(proj2) # Returns 500
Now, if you add a "bonus-based" project type, you just write calculate_budget.bonus. You don't touch the original generic function at all. The trade-off? S3 is loose. If you typo a class name as "houly" instead of "hourly", R won't complain until the function fails at runtime. It's fast to write, but it doesn't protect you from yourself.
S4: When the Stakes are Higher
If you're working in bioinformatics (Bioconductor) or building a package that other people will depend on for mission-critical math, S3 might be too flimsy. That's where S4 comes in. S4 is formal. You have to explicitly define the "slots" (the data types) that a class must have. It’s more verbose, but it catches errors early.
In S4, you define a setClass and use setGeneric and setMethod. It feels a bit more like Java or C#. You lose the "quick and dirty" speed of S3, but you gain a guarantee that every "Project" object actually has a total or a rate field before the function even tries to run. I generally suggest sticking to S3 unless you specifically need the strict validation S4 provides.
R6: Breaking the 'Copy-on-Modify' Rule
Here is the biggest conceptual jump: S3 and S4 are functional. When you modify an object, R creates a copy of that object. This is great for data integrity, but it's terrible for things like database connections or complex state machines where you want to update a single object in place.
R6 gives you "reference semantics." It behaves like a class in Python or JavaScript. You define a class with fields and methods, and when you call a method, it modifies the object itself without copying it. This is incredibly powerful for building APIs or managing a session.
library(R6)
ProjectManager <- R6Class("ProjectManager",
public = list(
projects = list(),
add_project = function(name, budget) {
self$projects[[name]] <- budget
cat("Project", name, "added!\n")
},
get_total_portfolio = function() {
sum(unlist(self$projects))
}
)
)
my_manager <- ProjectManager$new()
my_manager$add_project("Website", 5000)
my_manager$add_project("App", 12000)
my_manager$get_total_portfolio() # 17000
Notice the self$ syntax. We aren't passing the object into a function; we're calling a method on the object. Just be careful: because R6 objects are mutable, if you pass my_manager to another function and that function changes a value, it changes for everyone. That's a side-effect that can lead to some very confusing bugs if you're used to the standard R way of doing things.
📋 Practical Task
Build a State-Tracking API Client
You need to simulate a simple API client that tracks how many requests have been made to prevent hitting a rate limit. Using R6, create a class called ApiClient with the following specifications:
- A
publicfield calledrequest_countinitialized to 0. - A
publicmethod calledmake_request()that:- Increments the
request_countby 1. - Prints a message: "Request sent. Total requests: [count]".
- Increments the
- A
publicmethod calledreset_count()that setsrequest_countback to 0.
After defining the class, instantiate it as my_client, call make_request() three times, and then call reset_count() to verify the state changes correctly.
There are no comments for now.