13.6 Inheritance

Three ideas:

  1. Class is a vector of classes
class(ordered("x"))
#> [1] "ordered" "factor"
class(Sys.time())
#> [1] "POSIXct" "POSIXt"
  1. Dispatch moves through class vector until it finds a defined method
sloop::s3_dispatch(print(ordered("x")))
#>    print.ordered
#> => print.factor
#>  * print.default
  1. Method can delegate to another method via NextMethod(), which is indicated by -> as below:
sloop::s3_dispatch(ordered("x")[1])
#>    [.ordered
#> => [.factor
#>    [.default
#> -> [ (internal)

13.6.1 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.

sloop::s3_dispatch(y[1])
#>    [.secret
#>    [.default
#> => [ (internal)
y[1]
#> [1] 15

Fix this with a [.secret method:

The first fix (not run) is inefficient because it creates a copy of y.

# not run
`[.secret` <- function(x, i) {
  x <- unclass(x)
  new_secret(x[i])
}

NextMethod() is more efficient.

`[.secret` <- function(x, i) {
  # first, dispatch to `[`
  # then, coerce subset value to `secret` class
  new_secret(NextMethod())
}

Notice that [.secret is selected for dispatch, but that the method delegates to the internal [.

sloop::s3_dispatch(y[1])
#> => [.secret
#>    [.default
#> -> [ (internal)
y[1]
#> [1] "xx"

13.6.2 Allowing subclassing

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 subclasses
  • class for defining the subclass
new_secret <- function(x, ..., class = character()) {
  stopifnot(is.double(x))

  structure(
    x,
    ...,
    class = c(class, "secret")
  )
}

To create the subclass, simply invoke the parent class constructor inside of the subclass constructor:

new_supersecret <- function(x) {
  new_secret(x, class = "supersecret")
}

print.supersecret <- function(x, ...) {
  print(rep("xxxxx", length(x)))
  invisible(x)
}

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()