Theory:
What is class?
How to set class?
Some advice on style:
blob
, which defines a single class; haven has labelled
and haven_labelled
)_
; avoid .
since it might be confused as separator between generic and class namePractice:
How to compose a class in practice?
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
)
}
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?
length(levels) < max(values)
should be length(levels) < length(values)
, right?#> [1] a b c
#> Levels: a b c
#> Error: There must be at least as many `levels` as possible values in `x`
Some desired virtues:
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.
For convenience, we allow the user to also pass real values to a helper function.
Generic functions:
UseMethod()
UseMethod()
creates a vector of method namesWhile sloop::s3_dispatch()
gives the specific method selected for a specific call, on can see the methods defined:
#> # A tibble: 7 × 4
#> generic class visible source
#> <chr> <chr> <lgl> <chr>
#> 1 mean Date TRUE base
#> 2 mean default TRUE base
#> 3 mean difftime TRUE base
#> 4 mean POSIXct TRUE base
#> 5 mean POSIXlt TRUE base
#> 6 mean quosure FALSE registered S3method
#> 7 mean vctrs_vctr FALSE registered S3method
#> # A tibble: 4 × 4
#> generic class visible source
#> <chr> <chr> <lgl> <chr>
#> 1 as.data.frame ordered TRUE base
#> 2 Ops ordered TRUE base
#> 3 relevel ordered FALSE registered S3method
#> 4 Summary ordered TRUE base
Two rules:
...
Example from text:
I thought it would be good for us to work through this problem.
Carefully read the documentation for
UseMethod()
and explain why the following code returns the results that it does. What two usual rules of function evaluation doesUseMethod()
violate?
g <- function(x) {
x <- 10
y <- 10
UseMethod("g")
}
g.default <- function(x) c(x = x, y = y)
x <- 1
y <- 1
g(x)
#> x y
#> 1 1
#> x y
#> 1 1
Examples caught in the wild:
haven::zap_label
, which removes column labelsdplyr::mutate
tidyr::pivot_longer
Three ideas:
NextMethod()
, which is indicated by ->
as below:NextMethod()
Consider secret
class that masks each character of the input with x
in output
new_secret <- function(x = double()) {
stopifnot(is.double(x))
structure(x, class = "secret")
}
print.secret <- function(x, ...) {
print(strrep("x", nchar(x)))
invisible(x)
}
y <- new_secret(c(15, 1, 456))
y
#> [1] "xx" "x" "xxx"
Notice that the [
method is problematic in that it does not preserve the secret
class. Additionally, it returns 15
as the first element instead of xx
.
Fix this with a [.secret
method:
The first fix (not run) is inefficient because it creates a copy of y
.
NextMethod()
is more efficient.
Notice that [.secret
is selected for dispatch, but that the method delegates to the internal [
.
Continue the example above to have a supersecret
subclass that hides even the number of characters in the input (e.g., 123
-> xxxxx
, 12345678 -> xxxxx
, 1 -> xxxxx
).
To allow for this subclass, the constructor function needs to include two additional arguments:
...
for passing an arbitrary set of arguments to different subclassesclass
for defining the subclassTo create the subclass, simply invoke the parent class constructor inside of the subclass constructor:
But this means the subclass inherits all parent methods and needs to overwrite all parent methods with subclass methods that return the sublclass rather than the parent class.
There’s no easy solution to this problem in base R.
There is a solution in the vectors package: vctrs::vec_restore()