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.
Recommended Practices
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:
- A constructor in the form
new_myclass()
to create objects of the correct structure. - A user-friendly constructor
myclass()
that allows you to create objects in a convenient way. In our examples, this could be adog(name, age)
constructor that omits the class argument of the lower-level new_dog constructor. - 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
- Object-Oriented Programming (OOP) in R with R6 – The Complete Guide
- Unlocking the Power of Functional Programming in R (Part 1)
- Unlocking the Power of Functional Programming in R (Part 2): Key Concepts & Analytical Benefits
- Rhino for Shiny Developers: Top 5 Must-Have Software Engineering Skills
The post appeared first on appsilon.com/blog/.
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.