3 Coin Objects

3.1 Introduction

In this chapter we describe how to create object classes in R. Specifically, we will focus on the so-called S3 classes or S3 system. This is one of the three types of Object Oriented (OO) systems available in R, and it is the most common among R packages.

3.2 Objects and Classes

In the previous chapter we learned how to create a toss() function, and also how to document it with roxygen comments. So far, we have the following code:

#' Coin toss function
#'
#' 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)
}

We can invoke toss() to generate a first series of five tosses, and then compute the total proportion of heads:

# random seed
set.seed(534)

# five tosses
five <- toss(coin, times = 5)
five
#> [1] "tails" "tails" "tails" "heads" "heads"

# proportion of heads in five
sum(five == "heads") / length(five)
#> [1] 0.4

We can also get a second series of tosses, but this time involving tossing a coin six times. Similarly, we compute the total proportion of heads:

# six tosses
six <- toss(coin, times = 6)
six
#> [1] "heads" "heads" "heads" "tails" "heads" "tails"

# prop of heads in six
sum(six == "heads") / length(five)
#> [1] 0.8

The above code works … except that there is an error; the number of heads in six is being divided by 5 instead of 6. R hasn’t detected this error: it doesn’t know that the division has to be done using length(six).

Wouldn’t it be prefarable to have some mechanism that prevented this type of error from happening? Bugs will always be part of any programming activity, but it is better to minimize certain types of errors like the one above.

3.3 S3 Classes

R has two (plus one) object oriented systems, so it can be a bit intimidatin gwhen you read and learn about them for the first time. The goal of this section is not to make you an expert in all R’s OO systems, but to help you become familiar with the so-called “S3 class”.

S3 implements a style of object oriented programming called generic-function OO. S3 uses a special type of function called a generic function that decides which method to call. Keep in mind that S3 is a very casual system: it does not really have a formal definition of classes.

S3 classes are widely-used, in particular for statistical models in the "stats" package. S3 classes are very informal in that there is not a formal definition for an S3 class. Usually, S3 objects are built on top of lists, or atomic vectors with attributes. But you can also turn functions into S3 objects.

Note that in more formal OOP languages, all functions are associated with a class, while in R, only some are.

3.3.1 Making an object

To make an object an instance of a class, you just take an existing base object and set a "class" attribute for it. You can do that during creation of the object with the function structure() and its class argument. For example, we can create an object of class "coin" like so:

# object coin via structure()
coin1 <- structure(c("heads", "tails"), class = "coin") 
coin1
#> [1] "heads" "tails"
#> attr(,"class")
#> [1] "coin"

You can also create an object first, and then specify its class with the homonym function class():

# object coin via class()
coin2 <- c("heads", "tails")
class(coin2) <- "coin"
coin2
#> [1] "heads" "tails"
#> attr(,"class")
#> [1] "coin"

As any object in R, you can inspect the class of objects coin1 and coin2 with the class() function:

class(coin1)
#> [1] "coin"

class(coin2)
#> [1] "coin"

You can also determine if an object inherits from a specific class using inherits()

inherits(coin2, "coin")
#> [1] TRUE

Having a "coin" object, we can pass it to the toss() function to simulate flipping the coin:

toss(coin1, times = 5)
#> [1] "heads" "heads" "heads" "tails" "tails"

3.4 A more robust "coin" class

Let’s review our class "coin". The way we defined a coin object was like this:

# object coin
coin1 <- c("heads", "tails")
class(coin1) <- "coin" 

While this definition is good to illustrate the concept of an object, its class, and how to define generic methods, it is a very loose-defined class. One could create a "coin" out of c('tic', 'tac', 'toe'), and then use toss() on it:

ttt <- c('tic', 'tac', 'toe')
class(ttt) <- "coin"

toss(ttt)
#> [1] "tic"

We need a more formal definition of a coin object. For instance, it makes more sense to require that a coin should only have two sides. In this way, a vector like ttt would not be a valid coin.

For convenience purposes, we can define a class constructor function to initialize a "coin" object:

# constructor function (version 1)
coin <- function(object = c("heads", "tails")) {
  class(object) <- "coin"
  object
}

# default coin
coin()
#> [1] "heads" "tails"
#> attr(,"class")
#> [1] "coin"

# another coin
coin(c("h", "t"))
#> [1] "h" "t"
#> attr(,"class")
#> [1] "coin"

Think of this type of function as an auxiliary function that you can use to generate a default object of class coin.

3.5 Improving "coin" objects

To implement the requirement that a coin must have two sides, we can add an if() condition to the constructor function in order to check for the length of the input vector. If the length of the input object is different from two, then we stop execution; otherwise we proceed with the creation of a coin object.

# constructor function (version 2)
coin <- function(object = c("heads", "tails")) {
  if (length(object) != 2) {
    stop("\n'object' must be of length 2")
  }
  class(object) <- "coin"
  object
}

Let’s try our modified constructor function coin() to create a virtual version of the US penny like the one in the image below:

Example of a US penny (www.usacoinbook.com)

Figure 3.1: Example of a US penny (www.usacoinbook.com)

# US penny
penny <- coin(c("lincoln", "shield"))
penny
#> [1] "lincoln" "shield" 
#> attr(,"class")
#> [1] "coin"

Now let’s try coin() with an invalid input vector. In this case, the constructor function will stop() execution with an error message because the input argument has more than 2 elements.

# invalid coin
ttt <- c('tic', 'tac', 'toe')
coin(ttt)
#> Error in coin(ttt): 
#> 'object' must be of length 2

3.5.1 Attributes

Notice how everytime you print the name of a "coin" object, its class is displayed in the form of attr(,"class").

penny
#> [1] "lincoln" "shield" 
#> attr(,"class")
#> [1] "coin"

Interestingly, an R object can have multiple attributes. Right now our coin objects have just one attribute—its class. But we can add more attributes if we want to. For example, we could add an attribute prob. Let’s see why and how.

Recall that the toss() function simulates flips using sample(). Also, recall that one of the arguments of sample() is prob which lets you specify probabilities for each of the elements in the input vector. In order to take advantage of sample()’s argument prob, and being able to create loaded (i.e. biased) coins, we can add an attribute to our coin object to specify probabilities for each of its sides.

In other words, in addition to the class attribute of a coin, the idea is to assign another attribute for the probability values. We can do this by adding a prob argument to the constructor function, and then pass it as an attribute of the coin object inside the class-constructor function. Here’s how:

# constructor function (version 3)
coin <- function(object=c("heads", "tails"), prob=c(0.5, 0.5)) {
  if (length(object) != 2) {
    stop("\n'object' must be of length 2")
  }
  attr(object, "prob") <- prob
  class(object) <- "coin"
  return(object)
}

coin()
#> [1] "heads" "tails"
#> attr(,"prob")
#> [1] 0.5 0.5
#> attr(,"class")
#> [1] "coin"

In the previous code, the prob argument takes a vector of probabilities for each element in object. This vector is passed to object via the function attr() inside the body of coin(). Notice the use of a default argument prob = c(0.5, 0.5), that is, a fair coin by default.

3.5.2 Using a list

Another way to implement a constructor function coin() that returns an object containing values for both the sides and the probabilities, is to use an R list. Here’s the code for this option:

# constructor function (version 4)
coin <- function(sides=c("heads", "tails"), prob=c(0.5, 0.5)) {
  if (length(sides) != 2) {
    stop("\n'sides' must be of length 2")
  }
  res <- list(sides = sides, prob = prob)
  class(res) <- "coin"
  return(res)
}

coin()
#> $sides
#> [1] "heads" "tails"
#> 
#> $prob
#> [1] 0.5 0.5
#> 
#> attr(,"class")
#> [1] "coin"

Personally, I prefer this latter option because it allows you to create more complex objects as an R list. The important detail is to assign the name of a class to the created object with the function class().

3.5.3 Auxiliary Checker Function

Once again, while constructing an object of class "coin" we need to check its validity which involves checking for the validity of prob. We basically need to check that prob and its elements meet the following requirements:

  • must be numeric and of length 2

  • probability values must be between 0 and 1

  • the sum of these values must add up to 1

Here is one possible function to verify the aspects of prob listed above:

check_prob <- function(prob) {
  if (length(prob) != 2 | !is.numeric(prob)) {
    stop("\n'prob' must be a numeric vector of length 2")
  }
  if (any(prob < 0) | any(prob > 1)) {
    stop("\n'prob' values must be between 0 and 1")
  }
  if (sum(prob) != 1) {
    stop("\nelements in 'prob' must add up to 1")
  }
  TRUE
}

Note that I’m adding a TRUE statement at the end of the function. This is just an auxiliary value to determine whether the function returns a valid prob.

Now let’s test check_prob with valid and invalid values:

# Valid -----------------------
check_prob(c(0.5, 0.5))
#> [1] TRUE

check_prob(c(0.1, 0.9))
#> [1] TRUE

check_prob(c(1/3, 2/3))
#> [1] TRUE

check_prob(c(1/3, 6/9))
#> [1] TRUE
# Invalid -----------------------
# bad length
check_prob(1)
#> Error in check_prob(1): 
#> 'prob' must be a numeric vector of length 2

# bad length
check_prob(c(0.1, 0.2, 0.3))
#> Error in check_prob(c(0.1, 0.2, 0.3)): 
#> 'prob' must be a numeric vector of length 2

# negative probability
check_prob(c(-0.2, 0.8))
#> Error in check_prob(c(-0.2, 0.8)): 
#> 'prob' values must be between 0 and 1

# what should we do in this case?
check_prob(c(0.33, 0.66))     
#> Error in check_prob(c(0.33, 0.66)): 
#> elements in 'prob' must add up to 1

With the definition of the checker function check_prob(), we keep refining our constructor function coin():

# constructor function (version 5)
coin <- function(sides=c("heads", "tails"), prob=c(0.5, 0.5)) {
  if (length(sides) != 2) {
    stop("\n'sides' must be of length 2")
  }
  check_prob(prob)
  res <- list(sides = sides, prob = prob)
  class(res) <- "coin"
  return(res)
}

coin1 <- coin()
coin1
#> $sides
#> [1] "heads" "tails"
#> 
#> $prob
#> [1] 0.5 0.5
#> 
#> attr(,"class")
#> [1] "coin"

3.7 Extending classes

We can extend the class "coin" and create a derived class for special types of coins. For instance, say we want to create a class "quarter". One side of the coin refers to George Washington, while the other side refers to the bald eagle:

https://en.wikipedia.org/wiki/Quarter_(United_States_coin)

We can create a quarter by first starting with a coin() of sides washington and bald-eagle, and then assign a "quarter" class:

quarter1 <- coin(c("washington", "bald-eagle")) 
class(quarter1) <- c("quarter", "coin")
quarter1
#> object "coin"
#> 
#>         side prob
#> 1 washington  0.5
#> 2 bald-eagle  0.5

Interestingly, our coin quarter1 inherits from "coin":

inherits(quarter1, "coin")
#> [1] TRUE

In other words, quartier1 is of class "quarter" but it is also a "coin" object.

Likewise, we can create a class for a slightly unbalanced "dime":

dime1 <- coin(c("roosevelt", "torch"), prob = c(0.48, 0.52))
class(dime1) <- c("dime", "coin")
dime1
#> object "coin"
#> 
#>        side prob
#> 1 roosevelt 0.48
#> 2     torch 0.52

Here’s another coin example with a peso from Mexico (where I grew up). When you flip a peso, mexicans don’t really talk about about cara (heads) or cruz (tail). Instead, they say aguila (eagle) or sol (sun):

peso <- coin(c("aguila", "sol")) 
class(peso) <- c("peso", "coin")
peso
#> object "coin"
#> 
#>     side prob
#> 1 aguila  0.5
#> 2    sol  0.5