R S3 Class

S3 class implements a style of OO (Object Oriented) programming called generic- function OO. This is totally different from most programming languages, like C++, Java, and C# which implement message passing OO. With message passing messages or methods are sent to objects and the object determines which function to call. Typically, this object has an appearance in the method call, usually appearing before the name of the method or message. Like, canvas.drawRect(“blue”).

S3 is different. While computations are still carried out via methods, a special type of function called a generic function decides which method to call, e.g., drawRect(canvas, “blue”).

Defining S3 Class and Create S3 Objects

S3 has no formal definition of classes.

Example:

# create a list with required elements
s <- list(name = "Nikita", age = 24, GPA = 8.5)
# name the class appropriately
class(s) <- "student"
s

The above example will create an S3 class with the given list.

Output:

$name
[1] "Nikita"
$age
[1] 24
$GPA
[1] 8.5
attr(,"class")
[1] "student"

Creating Objects using constructors

You can easily convert your R object (vector, matrix, array, factor, list, data frame, or anything else) in the object of certain class type by simply attaching the ‘class’ attribute.

It is a good practice to use a function with the same name as a class to create objects.

Example:

Let's see an example, here we use the attr() function to set the class attribute of the object:

# a constructor function for the "student" class
student <- function(n,a,g) {
  # we can add our own integrity checks
  if(g>10 || g<0)  stop("GPA must be between 0 and 10")
    value <- list(name = n, age = a, GPA = g)  
  # class can be set using class() or attr() function
  attr(value, "class") <- "student"
  value
}

Let's create an object using this constructor:

> s <- student("Nikita", 24, 8.7)
> s

Output:

$name
[1] "Nikita"
$age
[1] 24
$GPA
[1] 8.7
attr(,"class")
[1] "student"

To know the class of object s:

> class(s)
[1] "student"

If you enter the GPA which is not in between 0 and 10 then it will display the error:

> s <- student("Nikita", 24, 12)
Error in student("Nikita", 24, 12) : GPA must be between 0 and 10

To change the value of GPA:

> s$GPA <- 5
> s
$name
[1] "Paul"
$age
[1] 26
$GPA
[1] 5
attr(,"class")
[1] "student"

Methods and Generic Functions

In the above example, when we simply write the name of the object, its contents get printed. Furthermore, we can use also use the print() function to print the contents of different type of objects like vectors, list, data frames, factors etc.

Now the question is how does print() know how to print these varieties of the dissimilar looking object? The answer is print() is a generic function. Actually, it has a number of methods. We can check all these methods with methods(print).

> methods(print)
[1] print.acf*                                           print.anova* 
[3] print.aov*                                           print.aovlist*
. 
.
.                                 
[189] print.undoc*                                       print.vignette*
[191] print.warnings                                     print.xgettext*
[193] print.xngettext*                                   print.xtabs*

In the above list we can find the methods like print.data.frame and print.factor.

When we call print() on a data frame, it is go to print.data.frame(). And when we call the same with a factor, the call will go to print.factor(). Hence, we can see that the method names are in the form generic_name.class_name(). This is how R is able to find out which method to call depending on the class.

Printing our object of class “student” looks for method of the form print.student(), but there is no method of this form. So, for this print.default() method is used. print.default() is a fallback method which is called if no other match is found.

Generic functions have a default method. There are several generic functions like print(). You can list them all with methods(class = "default").

[1] add1            aggregate       AIC             all.equal       ansari.test     anyDuplicated   aperm           ar.burg

…..

Create our own Method

Let's see an example to create a method print.student():

print.student <- function(obj) {
  cat("Name:", obj$name, "\n")
  cat("Age:", obj$age, "years old\n")
  cat("GPA:", obj$GPA, "\n")
}

Now, this method will be called whenever we print an object of class “student”.

> s
Name: Nikita
Age: 24 years old
GPA: 8.5

In S3 class system, method does not belong to object or class, they belong to generic functions. This will work as long as the class of object is set.

We can restore the class to their previous state by using unclass() function:

> unclass(s)
$name
[1] "Nikita"
$age
[1] 24
$GPA
[1] 8.5

Creating our own Generic Function

In R, it is possible to create our own generic function like print().

Let's see an example to create our own generic function called percent:

percent <- function(obj) {
  UseMethod("percent")
}

Here, useMethod() is used with the name of a generic function. This method is the dispatcher function which will handle all the background details.

Since a generic function is useless without any method. Let’s  implement the default method:

percent.default <- function(obj) {
  cat("This is a generic function\n")
}
Now, let’s create a method for our class student.
percent.student <- function(obj) {
  cat("Your percent is", obj$GPA, "\n")
}

Now, let’s run this generic function:

> percent(s)
Your percent is 8.5
Reference: https://www.cyclismo.org/tutorial/R/s3Classes.html