Object Oriented Programming in R Part 2: S3 Simplified

[This article was first published on Tag: r - Appsilon | Enterprise R Shiny Dashboards, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

In the previous article we made our first steps in Object Oriented Programming in R and learned that there are multiple ways of doing it.

In this article, we will dive deeper into the S3 system – the first object-oriented system in R.

Fun fact: if you have used R, you probably already interacted with some S3 classes and their methods, for example, factor and data.frame are classes available in R.

We will cover how it can be used, learn about its pros and cons as well as some recommended practices to consider when using them.

Table of Contents


Our First S3 class and First Method

We will reuse our examples from our OOP in R with R6 – The Complete Guide article. Let’s start by defining a function which will create objects of the dog class:

new_dog <- function(name, age) {
  structure(
    list(
      name = name,
      age = age
    ),
    class = "dog"
  )
}

Now, we can use this function to create our first dog!

d <- new_dog(name = "Milo", age = 4)

We can interact with our object using $ to retrieve fields of the object and try to print it:

d$name # Milo
d$age # 4

print(d)
$name
[1] "Milo"

$age
[1] 4

attr(,"class")
[1] "dog"

We can see that our dog gets printed out like a regular list. Let’s fix that by defining a print function for our dog class.

As the print generic function is already available in R; the only thing we need to do is to define a function with a specific naming scheme print.<NAME_OF_OUR_CLASS>:

print.dog <- function(x) {
  cat("Dog: \n")
  cat("\tName: ", x$name, "\n", sep = "")
  cat("\tAge: ", x$age, "\n", sep = "")
}

Now our dog class provides its own implementation of the print function. Let’s try to print our dog again:

print(d)

Dog: 
	Name: Milo
	Age: 4

Explore ‘R Data Processing Frameworks: How To Speed Up Your Data Processing Pipelines up to 20 Times‘—Elevate your data analysis efficiency.

Inheritance and Generics

Let’s make this example a bit more interesting and be inclusive of cat people as well. Both dogs and cats have names and make sounds (dogs say Woof while cats say Meow).

We will use inheritance to model this relationship by first defining an animal class:

new_animal <- function(name, age) {
  structure(
    list(
      name = name,
      age = age
    ),
    class = "animal"
  )
}

To add a subclass, we need to prepend the name of the subclass like this:

base_animal <- new_animal(name = "Milo", age = 4)
class(base_animal) <- c("Dog", class(base_animal))

Let’s make it neater by modifying new_animal to allow for adding subclasses:

new_animal <- function(name, age, class = character()) {
  structure(
    list(
      name = name,
      age = age
    ),
    class = c(class, "animal")
  )
}

Now, let’s create a constructor for our cat class:

new_cat <- function(name, age) {
  new_animal(
    name = name,
    age = age,
    class = "cat"
  )
}

The equivalent of our dog class would look like this:

new_dog <- function(name, age) {
  new_animal(
    name = name,
    age = age,
    class = "dog"
  )
}

Now, we want to be able to call the make_sound method on both our cat and dog classes. Let’s start by defining our first generic:

make_sound <- function(x) {
  UseMethod("make_sound")
}

You can think of a generic as defining a potential universal interaction (like the predict generic used for statistical models).

Now, we need to define a method for each class respectively, using the scheme <NAME_OF_OUR_GENERIC>.<NAME_OF_OUR_CLASS>:

make_sound.cat <- function(x) {
  cat(x$name, "says", "Meow!")
}

make_sound.dog <- function(x) {
  cat(x$name, "says", "Wooof!")
}

Now let’s check if it’s working:

c <- new_cat(name = "Tucker", age = 2)
d <- new_dog(name = "Milo", age = 4)

make_sound(c)
# Tucker says Meow!

make_sound(d)
# Milo says Wooof!

All right, it’s working! But what if we wanted to create classes for specific dog breeds, for example, Golden Retrievers? We will modify the new_dog constructor to allow for subclasses

new_dog <- function(name, age, class = character()) {
  new_animal(
    name = name,
    age = age,
    class = c(class, "dog")
  )
}

new_golden_retriever <- function(name, age) {
  new_dog(
    name = name,
    age = age,
    class = "golden_retriever"
  )
}

Now, we can create our first golden retriever, and all the dog functions will work as expected.

g <- new_golden_retriever(name = "Marley", age = 5)
make_sound(g)
# Marley says Wooof!

But hey, different breeds can Wooof slightly differently, right? Let’s indicate that by defining a method for golden retrievers:

make_sound.golden_retriever <- function(x) {
  cat(x$name, "says", "Wooof!", " (like a golden retriever)")
}

make_sound(g)
# Marley says Wooof!  (like a golden retriever)

A keen eye might notice that we have a repetition of printing our <NAME> says Woof!. Let’s fix that by leveraging inheritance:

make_sound.golden_retriever <- function(x) {
  NextMethod()
  cat(" (like a golden retriever)")
}

make_sound(g)
# Marley says Wooof!  (like a golden retriever)

The NextMethod call allows us to call the make_sound method of the parent class, which, in our case, is the dog class. Thanks to that, we avoided unnecessary repetitions in our code.

As we saw, S3 is a very simple and flexible system. However, flexibility comes at the cost of the possibility of shooting ourselves in the foot. For example, nothing stops you from making changes that would create incorrect objects:

number_dog <- 3
class(number_dog) <- c("dog")

print(number_dog)
# Error in x$name : $ operator is invalid for atomic vectors

This is why in Advanced R, Hadley recommends to provide the following functions when using S3 classes:

  1. A constructor in the form new_myclass() to create objects of the correct structure.
  2. A user-friendly constructor myclass() that allows you to create objects in a convenient way. In our examples, this could be a dog(name, age) constructor that omits the class argument of the lower-level new_dog constructor.
  3. A validate_myclass(), which checks if the object has the correct values

In addition, it might also be useful to provide an is_myclass function that checks if the object is of a given class.


Conclusion

The S3 system is the first object-oriented system in R. It provides the ability to create custom classes, generics and methods as well as use inheritance, which increases modularity and can reduce code repetitions.

The S3 system is very simple and flexible, which makes it easy to learn; however, without following recommended practices which enforce structure it can get a bit out of hand, so remember to provide constructors and validators for your S3 classes!

Did you find this article useful? Keep an eye out for the next one in this series and contact us for assistance with your enterprise Shiny and Data Science applications.

You May Also Like

The post appeared first on appsilon.com/blog/.

To leave a comment for the author, please follow the link and comment on their blog: Tag: r - Appsilon | Enterprise R Shiny Dashboards.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)