What Does "S3 Methods" Mean in R

What does S3 methods mean in R?

Most of the relevant information can be found by looking at ?S3 or ?UseMethod, but in a nutshell:

S3 refers to a scheme of method dispatching. If you've used R for a while, you'll notice that there are print, predict and summary methods for a lot of different kinds of objects.

In S3, this works by:

  • setting the class of objects of
    interest (e.g.: the return value of a
    call to method glm has class glm)
  • providing a method with the general
    name (e.g. print), then a dot, and
    then the classname (e.g.:
    print.glm)
  • some preparation has to have been
    done to this general name (print)
    for this to work, but if you're
    simply looking to conform yourself to
    existing method names, you don't need
    this (see the help I refered to
    earlier if you do).

To the eye of the beholder, and particularly, the user of your newly created funky model fitting package, it is much more convenient to be able to type predict(myfit, type="class") than predict.mykindoffit(myfit, type="class").

There is quite a bit more to it, but this should get you started. There are quite a few disadvantages to this way of dispatching methods based upon an attribute (class) of objects (and C purists probably lie awake at night in horror of it), but for a lot of situations, it works decently. With the current version of R, newer ways have been implemented (S4 and reference classes), but most people still (only) use S3.

How does R find S3 methods? Why can't R find my S3 `+` method?

If you aren't registering an S3 method as part of a package namespace or in the global environment, you'll need to explicitly register it using the .S3method() function. So in this case you would do

.S3method("+", "Foo", FooEnv$`+.Foo`)

This issue is further discussed here: https://developer.r-project.org/Blog/public/2019/08/19/s3-method-lookup/

Creating S3 methods in R

Create an object:

object <- list(coefficients = c("a" = 3, "b" = 4))

Assign the object a class:

class(object) <- "lad"

S3 methods have the form function.class. To define a "print" method:

print.lad <- function(object) {
print("Here are the coefficients:")
print(object$coefficients)
}

S3 methods are then automatically dispatched based on the object's class

print(object)
# [1] "Here are the coefficients:"
# a b
# 3 4

As an aside, I think I read somewhere that you should define print methods using cat instead of print because it's easier to control and nest, but I can't seem to find the source for that. For small cases, it shouldn't matter much.

What's the preferred means for defining an S3 method in an R package without introducing a dependency?

Fortunately, for R >= 3.6.0, you don't even need the answer by caldwellst. From the blog entry you linked above:

Since R 3.6.0, S3method() directives in NAMESPACE can also be used to perform delayed S3 method registration. With S3method(PKG::GEN, CLS, FUN) function FUN will get registered as an S3 method for class CLS and generic GEN from package PKG only when the namespace of PKG is loaded. This can be employed to deal with situations where the method is not “immediately” needed, and having to pre-load the namespace of pkg (and all its strong dependencies) in order to perform immediate registration is considered too “costly”.

Additionally, this is also discussed in the docs for the other suggestion of vctrs::s3_register():

#' For R 3.5.0 and later, `s3_register()` is also useful when demonstrating
#' class creation in a vignette, since method lookup no longer always involves
#' the lexical scope. For R 3.6.0 and later, you can achieve a similar effect
#' by using "delayed method registration", i.e. placing the following in your
#' `NAMESPACE` file:
#'
#' ```
#' if (getRversion() >= "3.6.0") {
#' S3method(package::generic, class)
#' }

So, you would simply need to not use @importFrom and instead of @export, use @exportS3Method package::generic (See https://github.com/r-lib/roxygen2/issues/796 and https://github.com/r-lib/roxygen2/commit/843432ddc05bc2dabc9b5b22c1ae7de507a00508)

Illustration

So, to illustrate, we can make two very simple packages, foo and bar. The package foo just has a generic foo() function and default method:

library(devtools)
create_package("foo")

#' foo generic
#'
#' @param x An object
#' @param ... Arguments passed to or from other methods
#' @export
foo <- function(x, ...) {
UseMethod("foo", x)
}
#' foo default method
#'
#' @param x An object
#' @param ... Arguments passed to or from other methods
#' @export
foo.default <- function(x, ...) {
print("Called default method for foo.")
}

After document() and install()ing, we create bar:

create_package("bar")

which creates a bar method for foo():

#' bar method for foo
#'
#' @param x A bar object
#' @param ... Arguments passed to or from other methods
#'
#' @exportS3Method foo::foo
foo.bar <- function(x, ...) {
print("Called bar method for foo.")
}

Importantly, we must load the foo package before running document(), or @exportS3Method won't work. That is,

library(foo)
document()

But, if we do that, we get the following in the NAMESPACE for bar:

# Generated by roxygen2: do not edit by hand

S3method(foo::foo,bar)

We have to manually add foo to "Suggests" in DESCRIPTION.

Then if we uninstall foo, we can still install bar:

> remove.packages("foo")
Removing package from ‘/home/duckmayr/R/x86_64-pc-linux-gnu-library/4.0’
(as ‘lib’ is unspecified)
> install("bar")
✓ checking for file ‘/home/jb/bar/DESCRIPTION’ ...
─ preparing ‘bar’:
✓ checking DESCRIPTION meta-information ...
─ checking for LF line-endings in source and make files and shell scripts
─ checking for empty or unneeded directories
─ building ‘bar_0.0.0.9000.tar.gz’

Running /opt/R/4.0.0/lib/R/bin/R CMD INSTALL \
/tmp/Rtmp5Xgwqf/bar_0.0.0.9000.tar.gz --install-tests
* installing to library ‘/home/jb/R/x86_64-pc-linux-gnu-library/4.0’
* installing *source* package ‘bar’ ...
** using staged installation
** R
** byte-compile and prepare package for lazy loading
** help
*** installing help indices
** building package indices
** testing if installed package can be loaded from temporary location
** testing if installed package can be loaded from final location
** testing if installed package keeps a record of temporary installation path
* DONE (bar)

Combining S4 and S3 methods in a single function

The section "Methods for S3 Generic Functions" of ?Methods suggest an S3 generic, an S3-style method for S4 classes, and the S4 method itself.

setClass("A")                    # define a class

f3 <- function(x, ...) # S3 generic, for S3 dispatch
UseMethod("f3")
setGeneric("f3") # S4 generic, for S4 dispatch, default is S3 generic
f3.A <- function(x, ...) {} # S3 method for S4 class
setMethod("f3", "A", f3.A) # S4 method for S4 class

The S3 generic is needed to dispatch S3 classes.

The setGeneric() sets the f3 (i.e., the S3 generic) as the default, and f3,ANY-method is actually the S3 generic. Since 'ANY' is at (sort of) the root of the class hierarchy, any object (e.g., S3 objects) for which an S4 method does not exist ends up at the S3 generic.

The definition of an S3 generic for an S4 class is described on the help page ?Methods. I think, approximately, that S3 doesn't know about S4 methods, so if one invokes the S3 generic (e.g., because one is in a package name space where the package knows about the S3 f3 but not the S4 f3) the f3 generic would not find the S4 method. I'm only the messenger.

Best practice for defining S3 methods with different arguments

The usual approach is to have a generic with no extra arguments except .... Each interface method should call to an underlying default method that implements the actual model-fitting.

optbin <- function(x, ...)
UseMethod("optbin")

optbin.formula <- function(formula, data, method, na.omit, arg1, arg2, ...)
{
...
optbin.default(x, y, arg1, arg2)
}

optbin.data.frame <- function(data, method, na.omit, arg1, arg2, ...)
{
...
optbin.default(x, y, arg1, arg2)
}

optbin.default <- function(x, y, arg1, arg2)
{ ... }

See for example how the nnet and MASS packages handle methods for formulas.



Related Topics



Leave a reply



Submit