OOP in R Exercises: 8 S3, S4 & R6 Practice Problems
Test your understanding of R's object-oriented systems with these 8 practice problems. They cover S3 (informal classes), S4 (formal classes), and R6 (reference classes), plus operator overloading and method dispatch. Each exercise includes a complete solution.
Exercise 1: S3 Class -- Temperature Converter
Create an S3 class Temperature that stores a value and a unit ("C" or "F"). Implement:
A constructor temperature(value, unit)
A print method that displays the value with its unit
A to_celsius() generic and method
A to_fahrenheit() generic and method
# Your code here
# temp <- temperature(100, "C")
# print(temp) # Should show: 100 C
# to_fahrenheit(temp) # Should return a Temperature in F
Create an S3 class Rational representing a fraction (numerator/denominator). Implement:
+ (addition of two Rationals)
* (multiplication)
== (equality after simplification)
print (shows "3/4" format)
Hint: Use gcd to simplify fractions.
Click to reveal solution
```r
gcd <- function(a, b) {
a <- abs(a); b <- abs(b)
while (b != 0) { temp <- b; b <- a %% b; a <- temp }
a
}
rational <- function(num, den) {
if (den == 0) stop("Denominator cannot be zero")
if (den < 0) { num <- -num; den <- -den }
g <- gcd(abs(num), den)
structure(list(num = num / g, den = den / g), class = "Rational")
}
print.Rational <- function(x, ...) cat(x$num, "/", x$den, "\n")
`+.Rational` <- function(e1, e2) {
rational(e1$num * e2$den + e2$num * e1$den, e1$den * e2$den)
}
`*.Rational` <- function(e1, e2) {
rational(e1$num * e2$num, e1$den * e2$den)
}
`==.Rational` <- function(e1, e2) {
e1$num == e2$num && e1$den == e2$den
}
# Test
a <- rational(1, 3)
b <- rational(1, 6)
print(a)
print(b)
cat("1/3 + 1/6 = "); print(a + b)
cat("1/3 * 1/6 = "); print(a * b)
cat("2/6 == 1/3:", rational(2, 6) == rational(1, 3), "\n")
Exercise 4: S4 Class -- Student Records
Create an S4 class Student with:
Slots: name (character), grades (numeric), graduation_year (integer)
Validity: grades must be between 0 and 100, graduation_year must be > 2000
A gpa() generic that computes mean grade
A show method for nice display
A honor_roll() generic that returns TRUE if GPA >= 85
Click to reveal solution
```r
setClass("Student",
slots = list(
name = "character",
grades = "numeric",
graduation_year = "integer"
),
validity = function(object) {
errors <- character()
if (any(object@grades < 0 | object@grades > 100)) {
errors <- c(errors, "grades must be between 0 and 100")
}
if (object@graduation_year <= 2000L) {
errors <- c(errors, "graduation_year must be > 2000")
}
if (length(errors) == 0) TRUE else errors
}
)
setGeneric("gpa", function(student) standardGeneric("gpa"))
setMethod("gpa", "Student", function(student) {
mean(student@grades)
})
setGeneric("honor_roll", function(student) standardGeneric("honor_roll"))
setMethod("honor_roll", "Student", function(student) {
gpa(student) >= 85
})
setMethod("show", "Student", function(object) {
cat("Student:", object@name, "\n")
cat(" GPA:", round(gpa(object), 1), "\n")
cat(" Graduation:", object@graduation_year, "\n")
cat(" Honor roll:", honor_roll(object), "\n")
})
# Test
alice <- new("Student",
name = "Alice",
grades = c(95, 88, 92, 87, 91),
graduation_year = 2026L
)
show(alice)
bob <- new("Student",
name = "Bob",
grades = c(70, 65, 80, 72, 68),
graduation_year = 2027L
)
show(bob)
Exercise 5: S4 Multiple Dispatch
Create two S4 classes Metric and Imperial for distance measurements. Implement an add_distance() generic with multiple dispatch that handles all four combinations (Metric+Metric, Imperial+Imperial, Metric+Imperial, Imperial+Metric), always returning Metric.
Q: Which OOP system should I learn first? Start with S3 -- it's the most common and the simplest. Learn R6 next if you need mutable objects. Learn S4 only when working with Bioconductor or packages that require formal types.
Q: How do I know if my class design is good? A good class has a clear responsibility, hides internal details (encapsulation), and its methods have obvious names. If you find yourself reaching into the internals of an object from outside the class, your design likely needs more methods.
Q: Can I mix S3, S4, and R6 in the same project? Yes, and many packages do. A common pattern is R6 for internal state management and S3 for user-facing objects that work with base R generics like print(), summary(), etc.