13.3 Classes
Theory:
What is class?
- No formal definition in S3
- Simply set class attribute
How to set class?
- At time of object creation
- After object creation
# at time of object creation
x <- structure(list(), class = "my_class")
# after object creation
x <- list()
class(x) <- "my_class"
Some advice on style:
- Rules: Can be any string
- Advice: Consider using/including package name to avoid collision with name of another class (e.g.,
blob
, which defines a single class; haven haslabelled
andhaven_labelled
) - Convention: letters and
_
; avoid.
since it might be confused as separator between generic and class name
Practice:
How to compose a class in practice?
- Constructor, which helps the developer create new object of target class. Provide always.
- Validator, which checks that values in constructor are valid. May not be necessary for simple classes.
- Helper, which helps users create new objects of target class. May be relevant only for user-facing classes.
13.3.1 Constructors
Help developers construct an object of the target class:
new_difftime <- function(x = double(), units = "secs") {
# check inputs
# issue generic system error if unexpected type or value
stopifnot(is.double(x))
units <- match.arg(units, c("secs", "mins", "hours", "days", "weeks"))
# construct instance of target class
structure(x,
class = "difftime",
units = units
)
}
13.3.2 Validators
Contrast a constructor, aimed at quickly creating instances of a class, which only checks type of inputs …
new_factor <- function(x = integer(), levels = character()) {
stopifnot(is.integer(x))
stopifnot(is.character(levels))
structure(
x,
levels = levels,
class = "factor"
)
}
# error messages are for system default and developer-facing
new_factor(1:5, "a")
#> Error in as.character.factor(x): malformed factor
… with a validator, aimed at emitting errors if inputs pose problems, which makes more expensive checks
validate_factor <- function(x) {
values <- unclass(x)
levels <- attr(x, "levels")
if (!all(!is.na(values) & values > 0)) {
stop(
"All `x` values must be non-missing and greater than zero",
call. = FALSE
)
}
if (length(levels) < max(values)) {
stop(
"There must be at least as many `levels` as possible values in `x`",
call. = FALSE
)
}
x
}
# error messages are informative and user-facing
validate_factor(new_factor(1:5, "a"))
#> Error: There must be at least as many `levels` as possible values in `x`
Maybe there is a typo in the validate_factor()
function? Do the integers need to start at 1 and be consecutive?
- If not, then
length(levels) < max(values)
should belength(levels) < length(values)
, right? - If so, why do the integers need to start at 1 and be consecutive? And if they need to be as such, we should tell the user, right?
13.3.3 Helpers
Some desired virtues:
- Have the same name as the class
- Call the constructor and validator, if the latter exists.
- Issue error informative, user-facing error messages
- Adopt thoughtful/useful defaults or type conversion
Exercise 5 in 13.3.4
Q: Read the documentation for utils::as.roman()
. How would you write a constructor for this class? Does it need a validator? What might a helper do?
A: This function transforms numeric input into Roman numbers. It is built on the integer type, which results in the following constructor.
The documentation tells us, that only values between 1 and 3899 are uniquely represented, which we then include in our validation function.
validate_roman <- function(x) {
values <- unclass(x)
if (any(values < 1 | values > 3899)) {
stop(
"Roman numbers must fall between 1 and 3899.",
call. = FALSE
)
}
x
}
For convenience, we allow the user to also pass real values to a helper function.