Object-Oriented Programming (OOP) in R with R6 – The Complete Guide


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

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(), and sleep() 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:

Image 1 – Contents of the object created from the Dog class

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)

Image 2 – Passing property values to a class without a constructor

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)

Image 3 – Contents of the object created from the Dog class (2)

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)

Image 4 – Accessing individual properties

Further, you can change property values belonging to an object by assigning a new value:

d$age = 5
print(d$age)

Image 5 – Assigning new property value

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)

Image 6 – Instantiating a class with private elements

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)

Image 7 – Attempting to access private class members outside the class

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")

Image 8 – Creating an instance of the class

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()

Image 9 – Calling a public method outside the class

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()

Image 10 – Attempting to call a private method outside the class

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)

Image 11 – Creating an instance of the child class

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!")

Image 12 – Accessing parent methods from a child class

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: