How to implement extracting/subsetting ([, [<-, [[, [[<-) functions for custom S3 classes?

Viewed 446

I have a custom S3 class foo, which adds some custom behavior on top of a normal data.frame:

foo_object <- data.frame()
class(foo_object) <- c("foo", "data.frame")

For this class there should also be a list class foo_list to store many of these foo objects.

foo_list_object <- list()
class(foo_list_object) <- c("foo_list", "list")

If you apply a subsetting operation for multiple elements on a list like this via foo_list_object[c(1,2,3)] you get an object only of class list. But I want it to remain a foo_list.

Hadley says you should implement custom accessor functions for vector classes to achieve this behavior:

When implementing a vector class, you should implement these methods: length, [, [<-, [[, [[<-, c. (If [ is implemented rev, head, and tail should all work).

My question concerning this has three parts:

  1. What's the best way to implement custom accessor functions for S3 classes? Can I rely on the underlying primitive class?
  2. Which accessor functions do I need in my case? I think I don't need to define [[, [<-, [[<- or $ because in these cases the inherited functionality of list behaves correctly also for foo_list. Should I implement them anyway?
  3. How can I implement an operator with 3 arguments like [<- or [[<-? You need the object, the index and the new value?

The following code for [ seems to work:

`[.foo_list` <- function(x, i) {
  class(x) <- "list"
  as.foo_list(x[i])
}

This on the other hand doesn't:

`[[<-.foo_list` <- function(x, i, y) {
  if (!is.foo(y)) {
    stop("Please provide an object of class foo.")
  }
  x[[i]] <- y
}

How can I fix this (1./3.) and do I even have to (2.)?

1 Answers

What's the best way to implement custom accessor functions for S3 classes? Can I rely on the underlying primitive class?

Yes, you can. Call NextMethod, inside your [<-.foo_list, and wrap the result in your class as needed.

Should I implement [all accessors] anyway?

If you want their result types to be foo_list or need to check the argument types, then yes, you need to implement the assignment accessors (but see below). You do not need to override the element read accessors ([[, $), unless you want to change their semantics: they inherit the correct behaviour from the list class.

How can I implement an operator with 3 arguments like [<- or [[<-?

Essentially like you’ve done. You only need to adjust the parameter names: the new value parameter needs to be named value because R internally calls the function with a named argument. And your last line would lead to an infinite recursion — you need to either unclass x first, or use NextMethod:

`[[<-.foo_list` <- function(x, i, value) {
  stopifnot(is.foo(value))
  NextMethod()
}

This works, and preserves the class type of x. Note that the only reason to implement this method is to perform the check in the first line. If you don’t need this check, you don’t need to implement the method at all.

In a similar vein, for your [ operator, the following simpler implementation works:

`[.foo_list` <- function(x, i) {
  as.foo_list(NextMethod())
}
Related