print, summary, and plot methods Min. 1st Qu. Median Mean 3rd Qu. Max.
-3.312878 -0.679415 0.006581 0.003209 0.684536 3.689426
mpg cyl disp hp
Min. :10.40 Min. :4.000 Min. : 71.1 Min. : 52.0
1st Qu.:15.43 1st Qu.:4.000 1st Qu.:120.8 1st Qu.: 96.5
Median :19.20 Median :6.000 Median :196.3 Median :123.0
Mean :20.09 Mean :6.188 Mean :230.7 Mean :146.7
3rd Qu.:22.80 3rd Qu.:8.000 3rd Qu.:326.0 3rd Qu.:180.0
Max. :33.90 Max. :8.000 Max. :472.0 Max. :335.0
drat wt qsec vs
Min. :2.760 Min. :1.513 Min. :14.50 Min. :0.0000
1st Qu.:3.080 1st Qu.:2.581 1st Qu.:16.89 1st Qu.:0.0000
Median :3.695 Median :3.325 Median :17.71 Median :0.0000
Mean :3.597 Mean :3.217 Mean :17.85 Mean :0.4375
3rd Qu.:3.920 3rd Qu.:3.610 3rd Qu.:18.90 3rd Qu.:1.0000
Max. :4.930 Max. :5.424 Max. :22.90 Max. :1.0000
am gear carb
Min. :0.0000 Min. :3.000 Min. :1.000
1st Qu.:0.0000 1st Qu.:3.000 1st Qu.:2.000
Median :0.0000 Median :4.000 Median :2.000
Mean :0.4062 Mean :3.688 Mean :2.812
3rd Qu.:1.0000 3rd Qu.:4.000 3rd Qu.:4.000
Max. :1.0000 Max. :5.000 Max. :8.000
Call:
lm(formula = mpg ~ wt, data = mtcars)
Residuals:
Min 1Q Median 3Q Max
-4.5432 -2.3647 -0.1252 1.4096 6.8727
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 37.2851 1.8776 19.858 < 2e-16 ***
wt -5.3445 0.5591 -9.559 1.29e-10 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 3.046 on 30 degrees of freedom
Multiple R-squared: 0.7528, Adjusted R-squared: 0.7446
F-statistic: 91.38 on 1 and 30 DF, p-value: 1.294e-10
The same function name. Completely different behaviour. No if/else in your code.
A generic is the user-facing function:
function (object, ...) UseMethod("summary")
It does almost nothing itself - it just dispatches.
The generic is the consistent interface. The methods are the specialised implementations.
When you call summary(x) and class(x) is "foo":
summary.foosummary.defaultAll four implement the same idea - dispatch based on class - but differ in formality and where methods live.
| System | Dispatch | Formality | Common in |
|---|---|---|---|
| S3 | Single (first arg) | Minimal | Base R, tidyverse, most packages |
| S4 | Multiple args | Formal classes, validity checks | Bioconductor |
| S7 | Multiple args | Formal, successor to S3 + S4 | Some newer packages |
| R6 | Single | Encapsulated, mutable | Shiny, databases, stateful objects |
S3 covers the vast majority of package development use cases. We focus on S3 today.
…with a class attribute attached.
cpue() a classCurrently cpue() returns a plain numeric vector. We can give it a class:
print methodWhen you type an object name at the console, R calls print() - this is autoprinting.
Generic Signature: print(x, ...) . . .
print.<classname>x... in the signature - the print generic requires it (See ?print)x; return invisible(x) so assignment still works: y <- print(x)cat() inside, not print() - calling print() inside print.foo causes infinite recursion@export so roxygen2 registers it in NAMESPACEA class tag alone gives us dispatch. Attributes let us attach context to the object.
Attributes can be read back with attr() or attributes():
structure() to set all attributes in one callConvention: name constructors new_<classname>. They are usually internal (@noRd).
Then cpue() calls the constructor instead of setting attributes directly:
print methodsummary methodsummary should print a useful statistical summary:
Generic Signature: summary(object, ...)
summary.cpue_result <- function(object, ...) {
cat("Survey Result Summary\n")
cat("---------------------\n")
cat("Method: ", attr(object, "method"), "\n")
cat("Records: ", attr(object, "n_records"), "\n")
cat("Gear factor: ", attr(object, "gear_factor"), "\n")
cat("Mean CPUE: ", round(mean(object), 2), "\n")
cat("Median CPUE: ", round(stats::median(object), 2), "\n")
cat("SD CPUE: ", round(stats::sd(object), 2), "\n")
invisible(object)
}plot() methodplot.<classname>, take x as the first argument, and include ... in the signature.plot() in a plot method - you can use ggplot2 or any other plotting system.Generic Signature: plot(x, ...)
The ... passes through to plot.default - users can customise colour, line type, etc. without us anticipating every option.
@export on each method tells roxygen2 to write the correct entry:
# NAMESPACE entries
S3method(print,cpue_result)
S3method(summary,cpue_result)
S3method(plot,cpue_result)
S3method() entries - not regular export() entries - are how R finds your methods when the package is installed.
cpue() now returns a cpue_result object, not a plain numeric.expect_equal() compares attributes, so tests that compare the result to a bare number will fail.We have a few options to fix this:
Wrap the result in as.numeric() for numeric comparisons:
expect_equal(as.numeric(cpue(100, 10)), 10).Use the ignore_attr = TRUE argument in expect_equal() to ignore attributes:
expect_equal(cpue(100, 10), 10, ignore_attr = TRUE).Write a helper function in tests/testthat/helper.R to use in our tests:
Then use expect_equal_numbers(cpue(100, 10), 10) in your tests.
expect_s3_class() checks the class:
Test that attributes are set correctly:
Commit your class definition, constructor, methods, and tests.
cpue() currently takes two numeric vectors. But users often have a data frame
cpue() a genericReplace the function body with UseMethod():
UseMethod("cpue") tells R: look at class(catch) and dispatch to cpue.<class>.
Move the existing implementation into cpue.numeric:
All existing calls to cpue() with a numeric first argument still work - R dispatches to cpue.numeric automatically.
#' @rdname cpue
#' @export
cpue.data.frame <- function(
catch,
...
) {
if (!"catch" %in% names(catch)) {
stop("Column 'catch' not found.", call. = FALSE)
}
if (!"effort" %in% names(catch)) {
stop("Column 'effort' not found.", call. = FALSE)
}
cpue(catch[["catch"]], effort = catch[["effort"]], ...)
}cpue() again with numeric vectors - which dispatches to cpue.numeric.With a default method, users get a clear error message when they call cpue() on a class for which no method is defined.
@rdname to group docsAll three methods share one help page:
#' Calculate CPUE
#' @param catch A numeric vector of catch, or a data frame.
#' @param ... Additional arguments passed to methods.
#' @export
cpue <- function(catch, ...) UseMethod("cpue")
#' @rdname cpue
#' ... same @param tags as before ...
#' @export
cpue.numeric <- function(catch, effort, ...) {
...
}
#' @rdname cpue
#' @export
cpue.data.frame <- function(catch, ...) {
...
}@rdname cpue tells roxygen2 to add documentation to the cpue help page rather than create a new one.
biomass_index() documentationbiomass_index to inherit parameters from cpue.numeric instead, since that is now the method it actually calls.cpue.numeric defines catch as a vector OR data.frame, but biomass_index expects a numeric vector
@param catch back explicitly:#' Calculate Biomass Index
#'
#' @param cpue Numeric vector of CPUE values. If NULL, computed from `catch`
#' and `effort`.
#' @param area_swept Numeric vector of area swept (e.g., km²).
#' @param catch Numeric vector of catch (e.g., kg).
#' @inheritParams cpue.numeric
#' @inheritDotParams cpue.numeric -effort
#' @export
biomass_index <- function(Commit the generic, methods, updated documentation, and tests.
print/summary/plotclass attributegeneric.class based on the class of the first argumentnew_<classname>) centralise object creation with structure()print, summary, and plot methods make your classes pleasant to work withUseMethod() for consistent interfaces across input types@export on each method registers it in NAMESPACE via S3method()expect_s3_class() and snapshot tests for print output