4 Methods (part 1)

4.1 Introduction

Having seen how to create S3 objects, in this chapter you will learn about how to create methods for S3 objects.

4.2 Improving toss()

From chapter 2, we ended up with the following toss() function:

#' @title Coin toss function 
#' @description Simulates tossing a coin a given number of times
#' @param x coin object (a vector)
#' @param times number of tosses
#' @param prob vector of probabilities for each side of the coin
#' @return vector of tosses
toss <- function(x, times = 1, prob = NULL) {
  sample(x, size = times, replace = TRUE, prob = prob)
}

The issue with the way toss() has been defined so far, is that you can provide any type of input vector (not necessarily of class "coin"), and it will still work. For instance, let’s bring back the vector c('tic', 'tac', 'toe') and use it as an input for toss()

toss(c('tic', 'tac', 'toe'))
#> [1] "toe"

The reason why toss() works with pretty much any vector, is because we are not checking for the validity of the input vector. That is, currently we are not enforcing the input vector to be an object of class "coin".

To create a function toss() that only works for objects of class "coin", we could add a stop() condition that checks if the argument x is of the right class:

toss <- function(x, times = 1, prob = NULL) {
  if (class(x) != "coin") {
    stop("\ntoss() requires an object 'coin'")
  }
  sample(x$sides, size = times, replace = TRUE, prob = prob)
}

# ok
toss(coin1)
#> [1] "tails"

# bad coin
toss(c('tic', 'tac', 'toe'))
#> Error in toss(c("tic", "tac", "toe")): 
#> toss() requires an object 'coin'

A more formal strategy, and one that follows OOP principles, is to create a toss method. In R, many functions are actually methods: e.g. print(), summary(), plot(), str(), etc. Out of curiosity, you can simply type the name of the function—without parenthesis—and confirm that print() is a method

# print method
print
#> function (x, ...) 
#> UseMethod("print")
#> <bytecode: 0x7fc314d47d58>
#> <environment: namespace:base>

The second line in the above output indicates UseMethod("print"), which is the way R tells you that print is a generic method. In fact, if you look at the manual documentation of print(), in the Description section you will see the following information

print prints its argument and returns it invisibly (via invisible(x)). It is a generic function which means that new printing methods can be easily added for new classes.

A function that is a generic method is not really one unique function but a collection or family of functions for printing objects, computing summaries, plotting, etc. Depending on the class of the object, a generic method will look for a specific function for that class. For example, objects of class "matrix" have several methods; to see the collection of available methods for this type of object use the methods() function:

# methods for objects "matrix"
methods(class = "matrix")
#>  [1] anyDuplicated as.data.frame as.raster     boxplot      
#>  [5] coerce        determinant   duplicated    edit         
#>  [9] head          initialize    isSymmetric   Math         
#> [13] Math2         Ops           relist        subset       
#> [17] summary       tail          unique       
#> see '?methods' for accessing help and source code

4.3 Generic Method toss

Let’s see how to to create methods for our coin tossing working example. When implementing new methods, you begin by creating a generic method with the function UseMethod():

# generic method 'toss'
toss <- function(x, ...) UseMethod("toss")

The function UseMethod() allows you to declare the name of a method. In this example we are telling R that the function toss() is now a generic "toss" method. Note the use of "..." in the function definition, this will allow you to include more arguments when you define specific methods based on "toss".

A generic method alone is not very useful. You need to create specific cases for the generic. In our example, we only have one class "coin", so that is the only class we will allow toss to be applied on. The way to do this is by defining toss.coin():

# specific method 'toss' for objects "coin"
toss.coin <- function(x, times = 1, prob = NULL) {
  sample(x$sides, size = times, replace = TRUE, prob = prob)
}

The name of the method, "toss", comes first, followed by a dot ".", followed by the name of the class, "coin". Notice that the body of the function toss.coin() does not include the stop() command anymore.

To use the toss() method on a "coin" object, you don’t really have to call toss.coin(); calling toss() is enough:

toss(coin1)
#> [1] "tails"

How does toss() work? Becasue toss() is now a generic method, everytime you use it, R will look at the class of the input, and see if there is an associated "toss" method. In the previous example, coin1 is an object of class "coin", for which there is a specific toss.coin() method. Thus using toss() on a "coin" object works fine.

Now let’s try toss() on the character vector c('tic', 'tac', 'toe'):

# no toss() method for regular vectors
toss(c('tic', 'tac', 'toe'))
#> Error in UseMethod("toss"): no applicable method for 'toss' applied to an object of class "character"

When you try to use toss() on an object that is not of class "coin", you get a nice error message like the one below

Error in UseMethod("toss"): no applicable method for 'toss' 
applied to an object of class "character"

Because an object "coin" already contains an element prob, the toss.coin() function does not really need an argument prob. Instead, we can pass this value from the coin object. Here’s a new definition of toss.coin():

toss.coin <- function(x, times = 1) {
  sample(x$sides, size = times, replace = TRUE, prob = x$prob)
}

Let’s toss a loaded coin:

set.seed(2341)
loaded_coin <- coin(c('HEADS', 'tails'), prob = c(0.75, 0.25))
toss(loaded_coin, times = 6)
#> [1] "HEADS" "HEADS" "HEADS" "HEADS" "HEADS" "tails"