Object-Oriented Programming (OOP) in R with R6 – The Complete Guide
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
If you want to talk about tried and tested programming paradigms, look no further than Object-Oriented Programming (OOP). The term was coined by Alan Kay way back in 1966, which means developers worldwide have been using it for more than five decades!
Today we’ll dive deep into the basics of OOP in R with R6. Now, what’s R6? It’s an R package that provides an implementation of object-oriented programming for the R language. More on that in a bit. We’ll first go over some fundamental OOP theory, and then talk about specifics, such as classes, objects, constructors, access members, inheritance, and so on.
Want to write R code that will last? Here are best practices for durable R code.
Table of contents:
- Object-Oriented Programming – The Fundamentals
- OOP in R with R6 – What is R6?
- Classes and Objects with R6
- Class Constructors
- Public and Private Class Members
- Class Methods
- Class Inheritance in R and R6
- Summary of OOP in R with R6
Object-Oriented Programming – The Fundamentals
As the name suggests, Object-Oriented Programming revolves around objects. We create these objects from blueprints named classes. Each class has properties and methods that describe how each object behaves. It’s that simple. There are only four building blocks to object-oriented programming:
- Classes: User-defined data types that serve as blueprints for creating objects, and their attributes and methods.
- Objects: Instances of individual classes. All dogs are dogs, but each dog is different from the other. They may share some common properties.
- Methods: Functions defined inside the class that describe the behavior of objects. A dog can bark, eat, and sleep; so you can implement corresponding
bark()
,eat()
, andsleep()
methods. - Properties: Things that describe a certain object. For example, a dog can be described by name, breed, hair color, weight, eye color, and so on. Those are the common properties for all dogs, but each dog can take a specific value for each property.
Are you following so far? Good, because things are about to get slightly complicated. In addition to the four building blocks of OOP, there are also four principles you must know. These are:
- Encapsulation: All important information is contained inside an object privately, and only selected information is exposed. No other object can access this class and change it. To keep up the analogy, think of a dog’s scent. It’s unique and cannot be changed by other dogs, but it can be sniffed by another.
- Polymorphism: Objects can take on more than one form. A dog is both an animal and a pet at the same time. Polymorphism allows us to perform a single action in different ways.
- Inheritance: Classes can reuse code from other classes by designing relationships (hierarchy) between them. A parent class of a Dog class would be an Animal, and a child class would be Samoyed.
- Abstraction: Objects should only reveal internal mechanisms relevant for the use of other objects. By doing so, developers can easily make changes and additions as the project scale in complexity.
There are some programming languages that treat everything as objects, such as Ruby and Scala. There are languages designed primarily for OOP, such as Java, and Python.
But where does R fit in? Let’s explore the ins and outs of OOP in R with the R6 package.
OOP in R with R6 – What is R6?
R6 is an R package that provides an implementation of object-oriented programming for R. It’s similar to R’s reference classes, but it’s more efficient and doesn’t depend on S4 classes and the methods package.
Instances of R6 classes have reference semantics and support public/private methods, active bindings, and inheritance.
Next, we’ll see the syntax of a typical R6 class and explain everything that goes into writing one.
Classes and Objects with R6
We’ll now create a simple class using OOP in R with the R6 package. To keep things simple, we’ll add only a handful of properties as a list to the public
argument. You can add properties and methods here, and they will be accessible to other objects outside the class:
library(R6) Dog <- R6Class( classname = "Dog", public = list( name = NULL, age = NULL, hair_color = NULL ) )
Now what? This is only a blueprint – and a poorly constructed one – but that’s the point. You can use this blueprint to create objects from the class. Store them to a separate variable. And use $new
every time you’re creating a new class instance:
d <- Dog$new() print(d)
Here’s what gets printed to the R console:
We’ve mentioned this class is poorly constructed because you can’t pass values to name
, age
, or hair_color
properties. We’ll need a special method called constructor for that.
Just for reference, here’s what happens if you try to pass values to the properties of a class that doesn’t have a constructor:
d <- Dog$new(name = "Milo", age = 4, hair_color = "black") print(d)
It’s a no-go. Let’s see how to work with constructors in R6 next.
Class Constructors
A constructor is a special method that’s executed whenever you create a new object of the class. So, each time you run d <- Dog$new()
, a new instance of the Dog
class is created, and therefore, the constructor method is executed.
In R and R6, the constructor is defined with the initialize
function. Its job is, in the most simple terms, to bind values passed in by the user to the instance properties – name
, age
, and hair_color
in our case. Constructors can do other things, but this is what you’ll do every time.
In R6, use self$property = property
to attach values passed by the user:
Dog <- R6Class( classname = "Dog", public = list( name = NULL, age = NULL, hair_color = NULL, initialize = function(name = NA, age = NA, hair_color = NA) { self$name = name self$age = age self$hair_color = hair_color } ) )
We can now use the code from the previous section to create an instance of a Dog
class with its respective property values:
d <- Dog$new(name = "Milo", age = 4, hair_color = "black") print(d)
You can now also access individual properties and methods of the class. For example, this is how you can print Milo’s name:
print(d$name)
Further, you can change property values belonging to an object by assigning a new value:
d$age = 5 print(d$age)
Now we’re getting somewhere, but there’s still a lot of ground to cover. Next, let’s see how private and public access modifiers work in OOP in R with R6.
Public and Private Class Members
The R6 package allows you to define private fields and methods, in addition to the public ones. What private means is that fields and methods can only be accessed within the class, and not from the outside.
To access private fields and methods within the class, use private$property
instead of self$property
. That’s the only difference.
We’ll demonstrate how the private access modifier works by making all of our three properties private. This means we’ll have to change how we access the properties in the constructor:
Dog <- R6Class( classname = "Dog", public = list( initialize = function(name = NA, age = NA, hair_color = NA) { private$name = name private$age = age private$hair_color = hair_color } ), private = list( name = NULL, age = NULL, hair_color = NULL ) )
Let’s create the same object from the class as before:
d <- Dog$new(name = "Milo", age = 4, hair_color = "black") print(d)
The console output is now significantly different. We see everything available inside and outside the class. We also see the values assigned to each property. But can we access them? Well, no, that’s the point of a private access modifier:
print(d$age)
Now you know the distinction between private and public in R and R6. We still haven’t covered one essential topic – methods. Let’s see how to declare both public and private ones.
Class Methods
We’ve managed to cover a lot of ground without even discussing class methods. Put simply, these are equivalent to plain old R functions with one twist – they belong to a class. Like properties, methods can be both public and private. Let’s see how they work.
The snippet below adds three methods to our Dog
class:
dog_age()
– Private method which returns the age of a dog multiplied by 7.bark()
– Public method that prints the name of the dog with a barking message. For demonstrations’ sake, we’ll call it from the constructor.show()
– Public method that prints details of a dog – its name, age, and hair color.
Dog <- R6Class( classname = "Dog", public = list( initialize = function(name = NA, age = NA, hair_color = NA) { private$name = name private$age = age private$hair_color = hair_color self$bark() }, bark = function() { cat(private$name, " says Woof!", sep = "") }, show = function() { cat("Dog: \n") cat("\tName: ", private$name, "\n", sep = "") cat("\tAge: ", private$age, " or ", private$dog_age(), " in dog years\n", sep = "") cat("\tHair color: ", private$hair_color, "\n", sep = "") } ), private = list( name = NULL, age = NULL, hair_color = NULL, dog_age = function() { return(private$age * 7) } ) )
And now let’s make an instance of this class. You’ll immediately see a message printed to the console, as the bark()
method was called from the constructor:
d <- Dog$new(name = "Milo", age = 4, hair_color = "black")
Remember that show()
is a public method, which means you can call it from outside the class. It will print the details about our dog:
d$show()
The same logic won’t work if you try to call a private method. You can’t access it outside the class, so you’ll get an error instead:
d$dog_age()
And that’s how methods work in OOP in R with R6. There’s one topic left for discussion today, and that’s inheritance.
Class Inheritance in R and R6
One R6 class can inherit from another, which means we can model the parent-child relationship. For example, all dogs are animals, but not all animals are dogs. Therefore, almost every animal has legs, but a dog is almost certain to have four of them.
For the sake of demonstration, we’ll create an Animal
class that describes the basic behavior of any animal. Every animal has a name (sort of), age, and a number of legs (not necessarily, but let’s stick with that logic). Also, let’s assume every animal can make a sound.
A Dog
can then inherit all the properties from the Animal
class and add its own. For example, we’ll add hair_color
property to the Dog
class.
Code-wise, the whole thing boils down to a couple of new things:
inherit
– A parameter that specifies from which class the child class will inherit from.super$initialize
– The way we call the constructor of the parent class. This is needed if we want to modify the constructor.
Here’s the code for both classes:
Animal <- R6Class( classname = "Animal", public = list( initialize = function(name = NA, age = NA, number_of_legs = NA) { private$name = name private$age = age private$number_of_legs = number_of_legs }, make_sound = function(sound) { cat(private$name, " says ", sound, "\n", sep = "") } ), private = list( name = NULL, age = NULL, number_of_legs = NULL ) ) Dog <- R6Class( classname = "Dog", inherit = Animal, public = list( initialize = function(name = NA, age = NA, number_of_legs = NA, hair_color = NA) { super$initialize(name, age, number_of_legs) private$hair_color = hair_color } ), private = list( hair_color = NULL ) )
Let’s make an instance of the Dog
class and see what happens:
d <- Dog$new(name = "Milo", age = 4, number_of_legs = 4, hair_color = "black") print(d)
We can see from the console output that our class inherits from some other class, alongside the public and private members.
Let’s try calling the make_sound()
function that’s only available in the parent class. If our logic is correct, any instance of the Dog
class will have access to it:
d$make_sound(sound = "Woooof!")
That’s the essence of inheritance. We model general behaviors in a parent class, and then inherit and slightly modify them in a child class. Easy!
Summary of OOP in R with R6
We won’t dive any deeper into object-oriented programming with R and R6 today. We covered a lot of ground, and this alone should be enough for you to model real-world problems and real-world behaviors.
At Appsilon, we find R super-flexible if you want to use an object-oriented programming paradigm, even though the language itself wasn’t necessarily designed for it. There’s nothing you can do in Java that you can’t do in R, even though the syntax is slightly different.
Now it’s time for you to shine. For a homework assignment, we recommend you model some real-world relationships with R and R6. For example, you could model cars. Every A8 is Audi, but not every Audi is an Audi A8, nor is every car an Audi. But every Audi is a car. Give it a go, and make sure to share your results with us on Twitter – @appsilon. We’d love to see what you come up with.
Also, if you want to see how object-oriented programming applies to R Shiny, look no further:
- Super Solutions for Shiny Apps #4: Using R6 Classes
- How to Build a Video Game in R Shiny with CSS, JavaScript, and R6 Classes
The post Object-Oriented Programming (OOP) in R with R6 – The Complete Guide appeared first on 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.