Skip to Content
Course content

122: S3 Classes and Methods In Depth

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've always found S3 to be one of the most "R" things about R. It's not a formal object-oriented system like Java or Python; it's essentially a polite agreement between the programmer and the R interpreter. If you give an object a specific class attribute, R will look for functions that match that class. Let's get our hands dirty by trying to build something that feels natural, and see where it breaks.

The List that Thinks it's Something Else

Imagine we're building a tool to track sensor readings. I want my readings to have a value and a unit, but I don't want them to just be anonymous lists. I want them to be "SensorReading" objects.

# I'll start by creating a simple list
my_reading <- list(value = 23.5, unit = "Celsius", timestamp = Sys.time())

# Now, I'll just tell R this is a 'SensorReading'
class(my_reading) <- "SensorReading"

print(my_reading)

If you run that, you'll notice something boring: it still prints exactly like a list. This is my first point of frustration. I've told R it's a SensorReading, but print() doesn't seem to care. Why? Because print is a generic function, and it hasn't been told how to handle my new class yet. It's falling back to the default method for lists.

Teaching R How to Print My Object

In S3, the convention is generic.class. If I want to change how print() behaves for SensorReading, I need to write a function called print.SensorReading.

print.SensorReading <- function(x, ...) {
  cat("Sensor Reading [", as.character(x$timestamp), "]\n", sep = "")
  cat("Value:", x$value, x$unit, "\n")
  invisible(x) # It's a good habit to return the object invisibly
}

# Let's try it again
print(my_reading)

Now it works! When I call print(my_reading), R looks at the class of my_reading, sees "SensorReading", and searches for a function named print.SensorReading. I didn't have to change the call to print(); I just changed the environment. This is the "magic" of S3 dispatch.

Building a Generic Dispatcher

Printing is great, but what if I want a custom action? Let's say I want a function called convert() that changes the units of my sensor reading. I might try this:

convert.SensorReading <- function(x, to_unit) {
  if (to_unit == "Fahrenheit" && x$unit == "Celsius") {
    x$value <- (x$value * 9/5) + 32
    x$unit <- "Fahrenheit"
  }
  return(x)
}

# I'll try to use it...
convert(my_reading, "Fahrenheit")
# Error in convert(my_reading, "Fahrenheit") : could not find function "convert"

And there it is—the classic S3 gotcha. I wrote the method, but I forgot to create the generic. print() was already a generic provided by R, but convert() doesn't exist. I need to tell R that convert is a function that should dispatch based on the class of its first argument.

convert <- function(x, ...) {
  UseMethod("convert")
}

# Now this should actually work
my_reading_f <- convert(my_reading, "Fahrenheit")
print(my_reading_f)

By calling UseMethod("convert"), I'm telling R: "Look at the class of x and find the corresponding convert.class function."

The Safety Net of Class Inheritance

One of the most powerful parts of S3 is that an object can have multiple classes. This creates a hierarchy. Let's say I have a specific type of sensor—a PressureSensor. I want it to use its own specific conversion logic, but if I don't provide one, I'm happy for it to use the general SensorReading logic.

# I'll give this object two classes
pressure_reading <- list(value = 1013, unit = "hPa", timestamp = Sys.time())
class(pressure_reading) <- c("PressureSensor", "SensorReading")

# I haven't defined print.PressureSensor, so what happens?
print(pressure_reading)

R looks for print.PressureSensor. It doesn't find it. Instead of crashing, it looks at the next class in the vector: SensorReading. It finds print.SensorReading and uses that. This is essentially inheritance. I can define a very specific behavior for a subclass, or let it "bubble up" to a more general class. It keeps the code DRY (Don't Repeat Yourself) and allows for flexible defaults.




📋 Practical Task

Building a Custom 'Employee' HR System

You need to build a small S3-based system to manage employee data. Your task is to implement the following:

  • Create a constructor function new_employee(name, salary, role) that returns a list with the class "Employee".
  • Create a generic function called calculate_bonus().
  • Implement a method calculate_bonus.Employee that returns 10% of the employee's salary.
  • Create a second class "Manager" that inherits from "Employee" (the class vector should be c("Manager", "Employee")).
  • Implement a method calculate_bonus.Manager that returns 20% of the salary.
  • Test your system: create one standard employee and one manager, and use calculate_bonus() on both to ensure the correct percentage is applied based on their class.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.