15.5 Constructors

It is good practice to use a function with the same name as class to create objects. This function is called the constructor of a class, because it constructs an object of this class. Actually, you’ve already used a lot of constructors during this tutorial. For example, when you type

> df <- data.frame(x = x)

then data.frame() is the constructor - it takes some input and returns an object of class data.frame!

Constructors are nice because they make code easily readable. In addition, you can add some integrity checks on the member attributes. Let’s go back to the student example and create a constructor for this class:

> student <- function(name, age, field, grades) {
+   # check if function arguments are valid
+   if (age < 0) 
+     stop("Age of student must be > 0!")
+   if (any(grades < 0 | grades > 6))
+     stop("Grades must be between 0 and 6!")
+ 
+   s <- list(name = name, age = age, field = field, grades = grades)
+   
+   # now set the class attribute
+   class(s) <- "student"
+   return(s)
+ }

We can now construct as many students as we want, in a very simple and clean way!

> # all ok
> liam <- student("Liam", 26, "biology", c(4.5, 6, 5.5))
> marco <- student("Marco", 28, "physics", c(5, 4, 5.5))
> 
> # not ok!
> carl <- student("Carl", -25, "philosophy", c(5.5, 5, 4))
Error in student("Carl", -25, "philosophy", c(5.5, 5, 4)): Age of student must be > 0!

In summary, to make a class (best practice):

  1. Write a constructor for your class.

  2. Write class methods for any generic function likely to use objects of your class.

The primary use of object-oriented programming in R is for print, summary and plot methods. These methods allow us to have one generic function, e.g. print(), that displays the object differently depending on its type. Imagine that you as an R user would have to call print.data.frame() if your object was a data frame, print.factor() if your object was a factor and so on. That would be super annoying, right? You would always first need to find out what class your object belongs to, and then call the corresponding function - a lot of complicated extra code. Thanks to S3 classes, no matter what class we’re dealing with, we can simply call print() - and R will figure out itself which underlying function it needs to call. The S3 system hence allows R functions to behave in different ways for different classes.

15.5.1 Exercises: S3 classes

See Section 18.0.39 for solutions.

  1. Write the constructor for a new class “circle”. The constructor accepts one argument: a number corresponding to the radius. The creation of the object should fail when you give invalid arguments. Check that your constructor is working by creating some objects of this class!

  2. Let’s add methods to our class circle. Implement a print method for the circle class. This method should print the radius of the circle object. Test it with your objects.

  3. Implement a plot method for the circle class. Hint: use the function grid.circle() from the library grid.

  4. Implement a generic function for class circle that calculates the perimeter (hint: this function should call UseMethod()), and the corresponding function with the special S3 naming. In the end, you should be able to call the function like this: perimeter(myCircle). Why do we have to write the generic function here, while we didn’t have to do this for print()?

  5. Following the same principle, implement a generic function for class circle that calculates the area.

  6. Check if the two functions above work by calling these functions on some objects of your class!

  7. Now, create another class “rectangle”. Rectangle should have the same methods as circle. This means, you should be able to create a rectangle object via a constructor (with two arguments, width and height), call perimeter() and area(), as well as print and plot (use grid.rect()) the rectangle object. Always check with examples if your class is working as expected.

  8. Say you’ve got a object “bobby” of a class “mammal”:

    > bobby <- list(name = "bobby", color = "brown", sex = "male")
    > class(bobby) <- "mammal"

    You now call print(bobby). What do you think will happen - which function will print() call? And which function will be called in the end?

  9. Define a print method for class “mammal” that prints the name of the mammal, the color and the sex Think first - what will happen if you now call print(bobby) again?

  10. Our mammal bobby is actually a cow. Assign a second class “cow” to bobby. What do you think will happen when you call print(bobby) now?

  11. Reverse the order of the class attribute vector of bobby. What do you think will happen when you call print(bobby)?

  12. We want print(bobby) to output “This is bobby. It’s a brown male cow”. What do you need to implement for this to happen?