Object Oriented Programming in R Part 2: S3 Simplified
In the <a href="https://appsilon.com/object-oriented-programming-in-r-part-1/" target="_blank" rel="noopener">previous article</a> 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 <strong>S3 system - the first object-oriented system in R</strong>.
<strong>Fun fact:</strong> if you have used R, you probably already interacted with some <strong>S3</strong> classes and their methods, for example, <code>factor</code> and <code>data.frame</code> are classes available in R.
<strong>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.</strong>
<h3>Table of Contents</h3>
<ul> <li><strong><a href="#our-first-s3-class-and-first-method">Our First S3 Class and First Method</a></strong></li> <li><strong><a href="#inheritance-and-generics">Inheritance and Generics</a></strong></li> <li><strong><a href="#recommended-practices">Recommended Practices</a></strong></li> <li><strong><a href="#conclusion">Conclusion</a></strong></li></ul>
<hr />
<h2 id="our-first-s3-class-and-first-method">Our First S3 class and First Method</h2>
We will reuse our examples from our <a href="https://appsilon.com/oop-in-r-with-r6/#oop-basics" target="_blank" rel="noopener">OOP in R with R6 - The Complete Guide</a> article. Let’s start by defining a function which will create objects of the <code>dog</code> class:
<pre><code class="language-r">
new_dog <- function(name, age) {
structure(
list(
name = name,
age = age
),
class = "dog"
)
}
</code></pre>
Now, we can use this function to create our first dog!
<pre><code class="language-r">
d <- new_dog(name = "Milo", age = 4)
</code></pre>
We can interact with our object using <code>$</code> to retrieve fields of the object and try to print it:
<pre><code class="language-r">
d$name # Milo
d$age # 4
<br>print(d)
$name
[1] "Milo"
<br>$age
[1] 4
<br>attr(,"class")
[1] "dog"
</code></pre>
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 <code>print</code> generic function is already available in R; the only thing we need to do is to define a function with a specific naming scheme <code>print.<NAME_OF_OUR_CLASS>:</code>
<pre><code class="language-r">
print.dog <- function(x) {
cat("Dog: \n")
cat("\tName: ", x$name, "\n", sep = "")
cat("\tAge: ", x$age, "\n", sep = "")
}
</code></pre>
Now our <code>dog</code> class provides its own implementation of the <code>print</code> function. Let’s try to print our dog again:
<pre><code class="language-r">
print(d)
<br>Dog:
Name: Milo
Age: 4
</code></pre>
<blockquote>Explore '<a href="https://appsilon.com/r-data-processing-frameworks/" target="_blank" rel="noopener">R Data Processing Frameworks: How To Speed Up Your Data Processing Pipelines up to 20 Times</a>'—Elevate your data analysis efficiency.</blockquote>
<h2 id="inheritance-and-generics">Inheritance and Generics</h2>
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 <em>Woof</em> while cats say <em>Meow</em>).
We will use <strong>inheritance</strong> to model this relationship by first defining an <code>animal</code> class:
<pre><code class="language-r">
new_animal <- function(name, age) {
structure(
list(
name = name,
age = age
),
class = "animal"
)
}
</code></pre>
To add a <strong>subclass</strong>, we need to prepend the name of the subclass like this:
<pre><code class="language-r">
base_animal <- new_animal(name = "Milo", age = 4)
class(base_animal) <- c("Dog", class(base_animal))
</code></pre>
Let’s make it neater by modifying <code>new_animal</code> to allow for adding subclasses:
<pre><code class="language-r">
new_animal <- function(name, age, class = character()) {
structure(
list(
name = name,
age = age
),
class = c(class, "animal")
)
}
</code></pre>
Now, let’s create a <strong>constructor</strong> for our <code>cat</code> class:
<pre><code class="language-r">
new_cat <- function(name, age) {
new_animal(
name = name,
age = age,
class = "cat"
)
}
</code></pre>
The equivalent of our <code>dog</code> class would look like this:
<pre><code class="language-r">
new_dog <- function(name, age) {
new_animal(
name = name,
age = age,
class = "dog"
)
}
</code></pre>
Now, we want to be able to call the <code>make_sound</code> method on both our <code>cat</code> and <code>dog</code> classes. Let’s start by defining our first <strong>generic</strong>:
<pre><code class="language-r">
make_sound <- function(x) {
UseMethod("make_sound")
}
</code></pre>
You can think of a <strong>generic as defining a potential universal interaction</strong> (like the <code>predict</code> generic used for statistical models).
Now, we need to define a <strong>method</strong> for each class respectively, using the scheme <code><NAME_OF_OUR_GENERIC>.<NAME_OF_OUR_CLASS></code>:
<pre><code class="language-r">
make_sound.cat <- function(x) {
cat(x$name, "says", "Meow!")
}
<br>make_sound.dog <- function(x) {
cat(x$name, "says", "Wooof!")
}
</code></pre>
Now let’s check if it’s working:
<pre><code class="language-r">
c <- new_cat(name = "Tucker", age = 2)
d <- new_dog(name = "Milo", age = 4)
<br>make_sound(c)
# Tucker says Meow!
<br>make_sound(d)
# Milo says Wooof!
</code></pre>
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 <code>new_dog</code> constructor to allow for subclasses
<pre><code class="language-r">
new_dog <- function(name, age, class = character()) {
new_animal(
name = name,
age = age,
class = c(class, "dog")
)
}
<br>new_golden_retriever <- function(name, age) {
new_dog(
name = name,
age = age,
class = "golden_retriever"
)
}
</code></pre>
Now, we can create our first golden retriever, and all the <code>dog</code> functions will work as expected.
<pre><code class="language-r">
g <- new_golden_retriever(name = "Marley", age = 5)
make_sound(g)
# Marley says Wooof!
</code></pre>
But hey, different breeds can <code>Wooof</code> slightly differently, right? Let’s indicate that by defining a method for golden retrievers:
<pre><code class="language-r">
make_sound.golden_retriever <- function(x) {
cat(x$name, "says", "Wooof!", " (like a golden retriever)")
}
<br>make_sound(g)
# Marley says Wooof! (like a golden retriever)
</code></pre>
A keen eye might notice that we have a repetition of printing our <code><NAME> says Woof!</code>. Let’s fix that by leveraging inheritance:
<pre><code class="language-r">
make_sound.golden_retriever <- function(x) {
NextMethod()
cat(" (like a golden retriever)")
}
<br>make_sound(g)
# Marley says Wooof! (like a golden retriever)
</code></pre>
The <code>NextMethod</code> call allows us to call the <code>make_sound</code> method of the parent class, which, in our case, is the <code>dog</code> class. Thanks to that, we avoided unnecessary repetitions in our code.
<h2 id="recommended-practices">Recommended Practices</h2>
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:
<pre><code class="language-r">
number_dog <- 3
class(number_dog) <- c("dog")
<br>print(number_dog)
# Error in x$name : $ operator is invalid for atomic vectors
</code></pre>
This is why in <a href="https://adv-r.hadley.nz/s3.html#s3-methods" target="_blank" rel="noopener noreferrer">Advanced R</a>, Hadley recommends to provide the following functions when using S3 classes:
<ol> <li>A constructor in the form <code>new_myclass()</code> to create objects of the correct structure.</li> <li>A user-friendly constructor <code>myclass()</code> that allows you to create objects in a convenient way. In our examples, this could be a <code>dog(name, age)</code> constructor that omits the class argument of the lower-level new_dog constructor.</li> <li>A <code>validate_myclass()</code>, which checks if the object has the correct values</li></ol>
In addition, it might also be useful to provide an <code>is_myclass</code> function that checks if the object is of a given class.
<hr />
<h2 id="conclusion">Conclusion</h2>
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 <a href="https://appsilon.com/#contact" target="_blank" rel="noopener">contact us for assistance</a> with your enterprise Shiny and Data Science applications.
<h2>You May Also Like</h2><ul> <li><a href="https://appsilon.com/oop-in-r-with-r6/" target="_blank" rel="noopener">Object-Oriented Programming (OOP) in R with R6 – The Complete Guide</a></li> <li><a href="https://appsilon.com/functional-programming-in-r-part-1/" target="_blank" rel="noopener">Unlocking the Power of Functional Programming in R (Part 1)</a></li> <li><a href="https://appsilon.com/functional-programming-in-r-part-2/" target="_blank" rel="noopener">Unlocking the Power of Functional Programming in R (Part 2): Key Concepts & Analytical Benefits</a></li> <li><a href="https://appsilon.com/rhino-r-software-engineering/" target="_blank" rel="noopener">Rhino for Shiny Developers: Top 5 Must-Have Software Engineering Skills</a></li></ul>