R lets you define what +, -, ==, [, and other operators do for your custom classes. This is called operator overloading. Both S3 and S4 support it, and it's how base R makes + work differently for numbers, dates, and strings.
When you type Sys.Date() + 1, R doesn't use the same + as 2 + 3. It dispatches to a method specialized for Date objects. You can define the same behavior for your own classes.
S3 Operator Overloading Basics
In S3, operators are generic functions. Define a method named operator.ClassName to overload it.
# A simple 2D point class
Point <- function(x, y) {
structure(list(x = x, y = y), class = "Point")
}
print.Point <- function(p, ...) {
cat("(", p$x, ",", p$y, ")\n")
}
# Overload + for Point
`+.Point` <- function(e1, e2) {
if (inherits(e2, "Point")) {
Point(e1$x + e2$x, e1$y + e2$y)
} else if (is.numeric(e2)) {
Point(e1$x + e2, e1$y + e2)
} else {
stop("Cannot add Point and ", class(e2))
}
}
# Overload - for Point
`-.Point` <- function(e1, e2) {
if (missing(e2)) {
Point(-e1$x, -e1$y) # Unary minus
} else if (inherits(e2, "Point")) {
Point(e1$x - e2$x, e1$y - e2$y)
} else {
stop("Cannot subtract")
}
}
# Overload == for Point
`==.Point` <- function(e1, e2) {
e1$x == e2$x && e1$y == e2$y
}
a <- Point(3, 4)
b <- Point(1, 2)
print(a)
print(a + b) # Point + Point
print(a + 10) # Point + scalar
print(a - b) # Point - Point
print(-a) # Unary minus
cat("a == b:", a == b, "\n")
cat("a == a:", a == Point(3, 4), "\n")
The Ops Group Generic
Instead of defining +.MyClass, -.MyClass, *.MyClass separately, you can define Ops.MyClass to handle all arithmetic and comparison operators at once.
# A Money class using the Ops group generic
Money <- function(amount, currency = "USD") {
structure(list(amount = amount, currency = currency), class = "Money")
}
print.Money <- function(x, ...) {
cat(x$currency, formatC(x$amount, format = "f", digits = 2), "\n")
}
Ops.Money <- function(e1, e2) {
# .Generic holds the operator being used
op <- .Generic
# Extract amounts
a1 <- if (inherits(e1, "Money")) e1$amount else e1
a2 <- if (inherits(e2, "Money")) e2$amount else e2
# Get currency (from whichever is Money)
curr <- if (inherits(e1, "Money")) e1$currency else e2$currency
# Check currency match for two Money objects
if (inherits(e1, "Money") && inherits(e2, "Money")) {
if (e1$currency != e2$currency) stop("Currency mismatch")
}
# Compute result
result <- switch(op,
"+" = a1 + a2,
"-" = a1 - a2,
"*" = a1 * a2,
"/" = a1 / a2,
"==" = a1 == a2,
"!=" = a1 != a2,
"<" = a1 < a2,
"<=" = a1 <= a2,
">" = a1 > a2,
">=" = a1 >= a2,
stop("Operator ", op, " not supported for Money")
)
# Comparisons return logical; arithmetic returns Money
if (op %in% c("==", "!=", "<", "<=", ">", ">=")) {
return(result)
}
Money(result, curr)
}
price <- Money(29.99)
tax <- Money(2.40)
print(price + tax)
print(price * 3)
cat("price > tax:", price > tax, "\n")
cat("price == price:", price == Money(29.99), "\n")
Overloading [ and [[
The subset operators [ and [[ are internal generics. You can define methods for them.
# A named collection class
Collection <- function(...) {
items <- list(...)
structure(items, class = "Collection")
}
print.Collection <- function(x, ...) {
cat("Collection with", length(x), "items:\n")
for (i in seq_along(x)) {
name <- names(x)[i]
if (is.null(name) || name == "") name <- paste0("[", i, "]")
cat(" ", name, ":", x[[i]], "\n")
}
}
# Override [ to return a Collection (not a plain list)
`[.Collection` <- function(x, i) {
result <- unclass(x)[i]
structure(result, class = "Collection")
}
# Override [[ for single element access
`[[.Collection` <- function(x, i) {
unclass(x)[[i]]
}
# Override length
length.Collection <- function(x) {
length(unclass(x))
}
coll <- Collection(a = 10, b = 20, c = 30, d = 40)
print(coll)
cat("\nSubset [2:3]:\n")
print(coll[2:3])
cat("\nElement [[1]]:", coll[[1]], "\n")
Overloading print, summary, and format
These are the most commonly overloaded generics. Every class should have at least a print method.
# A regression result class
RegResult <- function(formula, r_squared, coefficients) {
structure(
list(formula = formula, r_squared = r_squared, coefficients = coefficients),
class = "RegResult"
)
}
print.RegResult <- function(x, ...) {
cat("Linear Regression:", deparse(x$formula), "\n")
cat("R-squared:", round(x$r_squared, 4), "\n")
}
summary.RegResult <- function(object, ...) {
cat("=== Regression Summary ===\n")
cat("Formula:", deparse(object$formula), "\n")
cat("R-squared:", round(object$r_squared, 4), "\n")
cat("Coefficients:\n")
for (nm in names(object$coefficients)) {
cat(" ", nm, "=", round(object$coefficients[[nm]], 4), "\n")
}
}
format.RegResult <- function(x, ...) {
paste0("RegResult(R2=", round(x$r_squared, 3), ")")
}
# Use it
fit <- lm(mpg ~ wt + hp, data = mtcars)
result <- RegResult(
formula = mpg ~ wt + hp,
r_squared = summary(fit)$r.squared,
coefficients = as.list(coef(fit))
)
print(result)
cat("\n")
summary(result)
cat("\nFormatted:", format(result), "\n")
S4 Operator Overloading
S4 uses setMethod() for operators. The approach is more formal but the result is the same.
Q: Which operators can I overload in R? Almost all of them: arithmetic (+, -, *, /, ^, %%, %/%), comparison (==, !=, <, >, <=, >=), logical (&, |, !), subsetting ([, [[, $), and display (print, format, summary). You can also define custom infix operators like %between%.
Q: What happens when the left operand is not my class (e.g., 5 + MyObj)? For S3, R checks the class of both operands. If neither has a method, it falls back to the default. In your Ops method, use .Generic and check which argument has your class.
Q: Should I use Ops group generic or individual operator methods? Use the Ops group generic when the logic is similar for all operators (e.g., extract values, apply operator, wrap result). Use individual methods when different operators need very different logic.