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 methodglm
has classglm
) - 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
What's the Difference Between Integer Class and Numeric Class in R
Differencebetween Gc() and Rm()
How to Save Data File into .Rdata
How to Sort a Data Frame by Date
How to Fix the Aspect Ratio in Ggplot
Setting Document Title in Rmarkdown from Parameters
Rmarkdown: How to Change the Font Color
Geom_Text How to Position the Text on Bar as I Want
Sort a Data.Table Fast by Ascending/Descending Order
Text Clustering with Levenshtein Distances
Access and Preserve List Names in Lapply Function
How to Show the Y Value on Tooltip While Hover in Ggplot2
How to Add Frequency Count Labels to the Bars in a Bar Graph Using Ggplot2
Time Out an R Command via Something Like Try()