6

I have a question about the "right" way to do something with S3 classes in R. What I want to do is have a method that changes the class and then calls the same method on the new class. Something like this:

my_func <- function(.x, ...) {
  UseMethod("my_func")
}

my_func.character <- function(.x, ...) {
  return(paste(".x is", .x, "of class", class(.x)))
}

my_func.numeric <- function(.x, ...) {
  .x <- as.character(.x)
  res <- my_func(.x) # this should call my_func.character
  return(res)
}

And this works. When I do the following, I get class character for both

> my_func("hello")
[1] ".x is hello of class character"
> my_func(1)
[1] ".x is 1 of class character"

My question is: is this the right way to do this? Something feels weird about just re-calling the same method after I've converted the class (this line res <- my_func(.x)).

I felt like NextMethod() must somehow be the answer, but I've read a bunch of docs on it (this and this for example) but they all talk about this thing where it's jumping up to the next class in the list of classes, like from data.frame up to matrix when you have class(df) and get c("data.frame", "matrix") for example.

But none of them talk about this situation where you're converting it to an entirely different class that wasn't in the original hierarchy. So maybe NextMethod() isn't the right thing to use, but is there something else, or should I just leave it like I have it?

Thanks!

seth127
  • 2,594
  • 5
  • 30
  • 43
  • 1
    Though it seems weird, I think what you're doing here is fine. The only caveat I'd throw in is that you need to ensure there are no cycles in this recursive calling (you have none here). Using `NextMethod` is going to look for an S3 method on the next value within the `class` vector (if it exists); if not, it won't do what you need. – r2evans Mar 11 '20 at 17:19
  • One example of when `NextMethod` is appropriate can be found in [`tibble:::rownames<-.tbl_df`](https://github.com/tidyverse/tibble/blob/master/R/rownames.R). Since the class of the object is `c("tbl_df","tbl","data.frame")`, `rownames<-` first finds `tibble:::rownames<-.tbl_df`; when this calls `NextMethod()`, there is nothing for the second class (`rownames<-.tbl`), but it does find `rownames<-.data.frame` which it then calls. So I think `NextMethod()` is likely usable and a good idea when your object has multiple class inheritance (which they do not appear to, here). – r2evans Mar 11 '20 at 17:21

2 Answers2

3

1) If the idea is that my_func.numeric does additional processing but wants to make use of my_func.character as well without repeating that then in func.numeric set .Class and then call NextMethod like this:

my_func.numeric <- function(.x, ...) {
  .Class <- "character"
  .x <- as.character(.x)
  NextMethod()
}

NextMethod returns a result and additional processing can be optionally done after it. More information can be found by issuing ?NextMethod in R.

2) If the idea is that the numeric and character methods can be combined then:

my.func.character <- my.func.numeric <- function(.x, ...) {
    .x <- as.character(.x)
    paste(".x is", .x, "of class", class(.x))
}

It would also be possible to use the default method for this if that is not already being used for something else.

3) If the idea is just that there is shared functionality but you don't necessarily want to use all of the character method processing in the numeric method or visa versa then define a function called by each:

my_func_impl <- function(.x, ...) paste(".x is", .x, "of class", class(.x))

my_func.character <- function(.x, ...) {
  # some procesing unique to this method
  my_func_impl(.x, ...)
}

my_func.numeric <- function(.x, ...) {
  # some procesing unique to this method
  my_func_impl(.x, ...)
}
G. Grothendieck
  • 254,981
  • 17
  • 203
  • 341
0

The most standard way would be to create a default method, something like:

my_func.default <- function(.x, ...) {
  stopifnot(is.atomic(.x)) # throw an error if .x isn't an atomic vector
  .x <- as.character(.x)
  paste(".x is", .x, "of class", class(.x))
}
my_func("hello")
#> [1] ".x is hello of class character"
my_func(1)
#> [1] ".x is 1 of class character"
dave-edison
  • 3,666
  • 7
  • 19