S3 is the most commonly used object-oriented programming (OOP) system in R.
Most of the common data types you are used to are S3.
# Data frames are S3
::otype(mtcars) sloop
## [1] "S3"
# tibbles are S3
<- tibble::as_tibble(mtcars)
mt_tb ::otype(mt_tb) sloop
## [1] "S3"
# lm objects are S3
<- lm(mpg ~ wt, data = mtcars)
lmout ::otype(lmout) sloop
## [1] "S3"
# ggplot2 plots are S3
<- ggplot2::ggplot(mtcars, ggplot2::aes(x = wt, y = mpg)) +
pl ::geom_point()
ggplot2::otype(pl) sloop
## [1] "S3"
# tidymodels use S3
<-
tdout ::linear_reg() |>
parsnip::set_engine("lm") |>
parsnip::fit(mpg ~ wt, data = mtcars)
parsnip::otype(tdout) sloop
## [1] "S3"
# Factors are S3
<- factor(c(1, 2, 3))
x ::otype(x) sloop
## [1] "S3"
# Dates are S3
<- lubridate::make_date(year = 1970, month = 1, day = 1)
x ::otype(x) sloop
## [1] "S3"
If you are creating a package and you want OOP features, you should use S3 unless
This is since most R programmers are used to S3 (intuitively) and are not used to S4 or R6.
An S3 object is any variable with a class
attribute. This is the full definition.
S3 objects may or may not have more attributes.
E.g. the factor
class always has the levels
attribute.
<- factor(c("A", "B", "B", "A", "C", "A"))
x attributes(x)
## $levels
## [1] "A" "B" "C"
##
## $class
## [1] "factor"
You can get the underlying base type by unclass()
.
unclass(x)
## [1] 1 2 2 1 3 1
## attr(,"levels")
## [1] "A" "B" "C"
Functions can be S3 objects as well as long as they have the class
attribute.
<- stepfun(1:3, 0:3)
sout ::otype(sout) sloop
## [1] "S3"
class(sout)
## [1] "stepfun" "function"
S3 objects behave differently when passed to a generic function, a special type of function meant to provide different implementations based on the S3 class of the object.
Use sloop::ftype()
to see if a function is generic. If it has the word “generic” is anywhere, it can be used as an S3 generic.
These are all S3 generics
::ftype(print) sloop
## [1] "S3" "generic"
::ftype(summary) sloop
## [1] "S3" "generic"
::ftype(plot) sloop
## [1] "S3" "generic"
But these are not:
::ftype(lm) sloop
## [1] "function"
::ftype(stop) sloop
## [1] "internal"
Generic functions behave differently depending on the class of the object.
print(mt_tb)
## # A tibble: 32 × 11
## mpg cyl disp hp drat wt qsec vs am gear carb
## <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 21 6 160 110 3.9 2.62 16.5 0 1 4 4
## 2 21 6 160 110 3.9 2.88 17.0 0 1 4 4
## 3 22.8 4 108 93 3.85 2.32 18.6 1 1 4 1
## 4 21.4 6 258 110 3.08 3.22 19.4 1 0 3 1
## 5 18.7 8 360 175 3.15 3.44 17.0 0 0 3 2
## 6 18.1 6 225 105 2.76 3.46 20.2 1 0 3 1
## 7 14.3 8 360 245 3.21 3.57 15.8 0 0 3 4
## 8 24.4 4 147. 62 3.69 3.19 20 1 0 4 2
## 9 22.8 4 141. 95 3.92 3.15 22.9 1 0 4 2
## 10 19.2 6 168. 123 3.92 3.44 18.3 1 0 4 4
## # … with 22 more rows
print(lmout)
##
## Call:
## lm(formula = mpg ~ wt, data = mtcars)
##
## Coefficients:
## (Intercept) wt
## 37.29 -5.34
print(pl)
This is not implemented by if
-else
statements. That would be inefficient because only the authors of print()
(i.e. the R Core team) could add new functionality to new S3 objects. The idea of using generic functions allows us (new developers) to define new functionality to the same generics.
The implementation of a generic for a specific class is called a method.
The act of choosing a method from a generic is called method dispatch. Use sloop::s3_dispatch()
to see this process.
::s3_dispatch(print(mt_tb)) sloop
## print.tbl_df
## => print.tbl
## * print.data.frame
## * print.default
*
means the method exists but is not used.=>
means the method exists and is used.tbl_df
, so it moved on to tbl
which does have a method and used it. So it did not go on to look for other methods (data.frame
or the default
method), even those classes both have methods.Below there is no aperm()
method for matrices, integers, or numerics, so it used the default one, which is for arrays.
<- matrix(1:12, nrow = 4, ncol = 3)
mat ::s3_dispatch(aperm(mat, c(2, 1))) sloop
## aperm.matrix
## aperm.integer
## aperm.numeric
## => aperm.default
You can access specific methods by generic.class()
. E.g.
:::print.lm(lmout) stats
##
## Call:
## lm(formula = mpg ~ wt, data = mtcars)
##
## Coefficients:
## (Intercept) wt
## 37.29 -5.34
aperm.default(mat, c(2, 1))
## [,1] [,2] [,3] [,4]
## [1,] 1 2 3 4
## [2,] 5 6 7 8
## [3,] 9 10 11 12
But these are often not exported and should generally not be accessed directly by the user, or other developers.
Lots of methods have .
in the middle. But not all functions with .
are methods. E.g. read.csv()
and t.test()
are not methods of generic functions. read.csv()
is just a function with a dot in the name, and t.test()
is just a generic function with a dot in the name. These functions were created before S3, which is why they are named poorly.
You can confirm that a function with a .
in it is a method also with sloop::is_s3_method()
.
::is_s3_method("read.csv") sloop
## [1] FALSE
::is_s3_method("t.test") sloop
## [1] FALSE
::is_s3_method("print.default") sloop
## [1] TRUE
Because of the important role of .
, you should never name variables or non method functions with a dot in them.
To find all of the methods of a generic, use sloop::s3_methods_generic()
.
::s3_methods_generic("print") sloop
## # A tibble: 314 × 4
## generic class visible source
## <chr> <chr> <lgl> <chr>
## 1 print acf FALSE registered S3method
## 2 print AES FALSE registered S3method
## 3 print all_vars FALSE registered S3method
## 4 print anova FALSE registered S3method
## 5 print any_vars FALSE registered S3method
## 6 print aov FALSE registered S3method
## 7 print aovlist FALSE registered S3method
## 8 print ar FALSE registered S3method
## 9 print Arima FALSE registered S3method
## 10 print arima0 FALSE registered S3method
## # … with 304 more rows
To find all methods for a class, use sloop::s3_methods_class()
.
::s3_methods_class("data.frame") sloop
## # A tibble: 55 × 4
## generic class visible source
## <chr> <chr> <lgl> <chr>
## 1 [ data.frame TRUE base
## 2 [[ data.frame TRUE base
## 3 [[<- data.frame TRUE base
## 4 [<- data.frame TRUE base
## 5 $<- data.frame TRUE base
## 6 aggregate data.frame TRUE stats
## 7 anyDuplicated data.frame TRUE base
## 8 anyNA data.frame TRUE base
## 9 as.data.frame data.frame TRUE base
## 10 as.list data.frame TRUE base
## # … with 45 more rows
Exercise: Explain the difference between each of the dots in as.data.frame.data.frame()
. How would you typically use this method? Include in your discussion calls from the functions in the {sloop}
package.
Exercise: mean()
is an S3 generic. What classes have a method for mean()
. What is the difference between them?
Exercise (Advanced R): What class of object does the following code return? What base type is it built on? What attributes does it use?
set.seed(21)
<- ecdf(rpois(100, 10))
x x
## Empirical CDF
## Call: ecdf(rpois(100, 10))
## x[1:14] = 4, 5, 6, ..., 16, 17
Exercise: (Advanced R): What class of object does the following code return? What base type is it built on? What attributes does it use?
<- table(rpois(100, 5))
x x
##
## 1 2 3 4 5 6 7 8 9 10 12
## 1 11 17 13 11 22 8 11 4 1 1
Again, an S3 object is any object with a class attribute, that you can create with:
# Create and assign class in one step
<- structure(list(), class = "my_class")
x
# Create, then set class
<- list()
x class(x) <- "my_class"
You can get the class attribute by class()
(as long as it is S3).
class(x)
## [1] "my_class"
Thus, it is a little safer to use sloop::s3_class()
.
::s3_class(x) sloop
## [1] "my_class"
You can test that an object is a certain class by inherits()
.
class(mtcars)
## [1] "data.frame"
inherits(mtcars, "data.frame")
## [1] TRUE
inherits(mtcars, "tibble")
## [1] FALSE
<- tibble::as_tibble(mtcars)
mt_tb inherits(mt_tb, "tbl_df")
## [1] TRUE
inherits(mt_tb, "data.frame")
## [1] TRUE
R has no checks that the structure of the class is as you intended. E.g., we can change the “data.frame” class to "Date"
and bad things will happen (i.e. R will try to use the wrong generics on the data).
class(mt_tb) <- "Date"
mt_tb
## Error in as.POSIXlt.Date(x): 'list' object cannot be coerced to type 'double'
You have to be careful about enforcing the correct structure on your class. Best practice: For any S3 class you create, you should create 3 functions to help others build and validate your class:
Your constructor should
new_myclass()
, replacing “myclass” with the name of your class.E.g. if we were to create our own class to recapitulate factors, called factor2
, we would do
<- function(x = integer(), levels = character()) {
new_factor2 stopifnot(is.integer(x))
stopifnot(is.character(levels))
return(
structure(x,
levels = levels,
class = "factor2")
) }
We can construct a factor as follows
<- new_factor2(c(1L, 1L, 2L, 1L, 1L), levels = c("A", "B"))
x x
## [1] 1 1 2 1 1
## attr(,"levels")
## [1] "A" "B"
## attr(,"class")
## [1] "factor2"
Making sure the structure of an object is what you would expect is expensive.
E.g., we need to make sure that the number of unique values in a factor is at most the number of levels in that factor.
Validator functions should:
validator_myclass()
.Let’s make a validator for factor2
.
<- function(x) {
validate_factor2 stopifnot(inherits(x, "factor2"))
<- unclass(x)
values <- attr(x, "levels")
levels
if (length(levels) < max(values)) {
stop("There must be at least as many `levels` as possible values in `x`")
}return(x)
}
validate_factor2(x)
## [1] 1 1 2 1 1
## attr(,"levels")
## [1] "A" "B"
## attr(,"class")
## [1] "factor2"
A helper function is a user-facing function that will
myclass()
.Let’s do this for factor2
.
<- function(x = character(), levels = unique(x)) {
factor2 <- match(x, levels)
ind return(validate_factor2(new_factor2(ind, levels)))
}
factor2(c("A", "B", "B", "A"))
## [1] 1 2 2 1
## attr(,"levels")
## [1] "A" "B"
## attr(,"class")
## [1] "factor2"
Side note: match()
is a useful function. It will provide the positions of the second argument that match the values in the second argument. E.g.
match(c("A", "A", "B", "A", "B"), c("A", "B"))
## [1] 1 1 2 1 2
Exercise (Advanced R): Write a constructor for data.frame objects. What base type is a data frame built on? What attributes does it use? What are the restrictions placed on the individual elements? What about the names?
A generic function is just one that performs method dispatch. Method dispatch is implemented through UseMethod()
, so it is really easy to create a new generic.
<- function(x, ...) {
mygeneric UseMethod("mygeneric")
}
No arguments are passed to UseMethod()
except the name of the generic.
The x
is a required argument that all methods must have. You can choose to have this be a different name, to have more required arguments, or to have no required arguments.
The ...
allows methods of your generic to include other variables than just x
.
This is literally what most generic function definitions look like.
mean
## function (x, ...)
## UseMethod("mean")
## <bytecode: 0x557943aeaa50>
## <environment: namespace:base>
print
## function (x, ...)
## UseMethod("print")
## <bytecode: 0x557943bf9c90>
## <environment: namespace:base>
plot
## function (x, y, ...)
## UseMethod("plot")
## <bytecode: 0x55794372b5e8>
## <environment: namespace:base>
summary
## function (object, ...)
## UseMethod("summary")
## <bytecode: 0x55794779c4c0>
## <environment: namespace:base>
The key of a generic is its goals. Methods should generally align with the goals of the generic so that R users don’t get unexpected results. E.g. when you type plot()
you shouldn’t output a mean (even though S3 makes this valid behavior).
How UseMethod()
works: If an object has a class vector of c("cl1", "cl2")
then UseMethod()
will first search for a method for cl1
, if it does not exist it will use the method for cl2
, and if that does not exist it will use the default method (there is usually one).
E.g. all tibbles have class
<- tibble::as_tibble(mtcars)
mt_tb class(mt_tb)
## [1] "tbl_df" "tbl" "data.frame"
So any generic called with a tibble will first search for a tbl_df
method, then a tbl
method, then a data.frame
method, then a default method (which would be for a list if applicable since tibbles are built on lists).
::s3_dispatch(print(mt_tb)) sloop
## print.tbl_df
## => print.tbl
## * print.data.frame
## * print.default
::s3_dispatch(str(mt_tb)) sloop
## => str.tbl_df
## str.tbl
## * str.data.frame
## * str.default
::s3_dispatch(summary(mt_tb)) sloop
## summary.tbl_df
## summary.tbl
## => summary.data.frame
## * summary.default
::s3_dispatch(mean(mt_tb)) sloop
## mean.tbl_df
## mean.tbl
## mean.data.frame
## => mean.default
The “default” class is not a real class, but is there so that there is always a fall back.
To create a method
generic.method()
....
in the generic).E.g., let’s create plot and print methods for our factor2
class.
<- function(x) {
print.factor2 print(attr(x, "levels")[x])
return(invisible(x))
}
<- function(x, y = NULL) {
plot.factor2 <- table(attr(x, "levels")[x])
tabx barplot(table(attr(x, "levels")[x]))
return(invisible(x))
}
Now, we get better printing for factor2
’s
<- factor2(c("A", "A", "B", "B", "A", "B"))
x print(x)
## [1] "A" "A" "B" "B" "A" "B"
Note: If you don’t know, whenever you just run something and have it print to the console, that is R implicitly running print()
. So this looks better too:
x
## [1] "A" "A" "B" "B" "A" "B"
Note: In a print method, you either call the print()
method of another S3 object, or you call cat()
, which does less under the hood than print()
.
We can verify that method dispatch is working appropriately
::s3_dispatch(print(x)) sloop
## => print.factor2
## * print.default
Plotting looks better too
plot(x)
You should only build methods for classes you own, or generics you own. It is considered bad manners to define a method for a class you do not own unless you own the generics.
E.g. if you define a new print method for tbl_df
, then include that in your package, that would be impolite to the tidyverse folks.
A method should have the same arguments as the generic. You can have more arguments if the generic has ...
in it. E.g. if you create plot()
, then you must include x
and y
, but may include anything else.
formals(plot)
## $x
##
##
## $y
##
##
## $...
Exercise (Advanced R): What generics does the table
class have methods for?
Exercise: Create a new generic called pop
that will remove the last element and return the shorted object. Make a default method for any vector. Then make methods for the matrix
class that will remove the last column or row, depending on the user choice of an argument called by
.
There are three most common structures for an S3 object.
In decreasing order of most common usage by you:
A list-like object, where the list represents one thing (e.g. model output, function, dataset, etc…).
lm()
is a list like object that represents one model fit.<- lm(mpg ~ wt, data = mtcars)
lmout ::otype(lmout) sloop
## [1] "S3"
typeof(lmout)
## [1] "list"
A vector with new functionality. E.g. factor
s and Date
s. You combine, print, mathematically operate with these vectors in different ways.
<- factor(c("A", "A", "B", "A", "B"))
x ::otype(x) sloop
## [1] "S3"
typeof(x)
## [1] "integer"
Lists of equal length length vectors. E.g. data.frame
s and POSIXlt
objects.
POSIXlt
objects are lists of years, days, minutes, seconds, etc… with the i
th element of each vector contributing to indicating the same moment in time.<- as.POSIXlt(ISOdatetime(2020, 1, 1, 0, 0, 1:3))
x x
## [1] "2020-01-01 00:00:01 EST" "2020-01-01 00:00:02 EST"
## [3] "2020-01-01 00:00:03 EST"
typeof(x)
## [1] "list"
data.frame
objects are lists of vectors where each vector is a variable and the i
th element of each vector represents the same observational unit.typeof(mtcars)
## [1] "list"
Inheritance is shared behavior. You can make your new class inherit from another class so that if you did not create a method, then it will fall back on the parent method.
We call the child class the subclass and the parent class the superclass.
E.g. the tbl_df
(sub)class inherits from the data.frame
(super)class.
You can simply create a subclass by including a vector of in the class
attribute.
<- tibble::as_tibble(mtcars)
mt_tb class(mt_tb)
## [1] "tbl_df" "tbl" "data.frame"
You should make sure your subclass is of the same base type as the superclass you are inheriting from. E.g. make sure anything you build off of data.frame
s also has a list base type.
You should make sure that you have at least all of the same attributes as the superclass you are inheriting from. E.g. data.frame
s can have names
and row.names
, and so any subclass should also have those attributes.
NextMethod()
allows you define methods for your class that use the functionality of classes that you inherit from.
E.g. recall that most attributes are lost with [
.
<- factor2(c("A", "A", "B", "A", "B"))
x 1] x[
## [1] 1
This is because R is using the integer version for [
and so we lose the class.
::s3_dispatch(x[1]) sloop
## [.factor2
## [.default
## => [ (internal)
We cannot use [
inside a definition for a method because we haven’t defined it yet.
## won't work
`[.factor2` <- function(x, i) {
return(x[i]) # but we haven't defined `[` yet
}1] ## infinite recursion x[
You can define your method to use the next method by NextMethod()
. NextMethod()
will take the arguments inside your function definition and run them through the next method in the inheritance list. So it returns an unclassed object, that you can then pass to your constructor function.
Make sure you also include the attributes in your constructor.
`[.factor2` <- function(x, i) {
new_factor2(NextMethod(), levels = attr(x, "levels"))
}
::s3_dispatch(x[1:3]) sloop
## => [.factor2
## [.default
## -> [ (internal)
1:3] x[
## [1] "A" "A" "B"
If we did not pass NextMethod()
to our constructor, it would just run the integer subsetting:
`[.factor2` <- function(x, i) {
NextMethod()
}1:3] x[
## [1] 1 1 2
Let’s work again on an a simulation function for linear regression. We started this in the Assertions and Unit Tests lecture.
The basic function we had was as follows:
<- function(x, beta0, beta1, sigma) {
simreg <- length(x)
n <- stats::rnorm(n = n, mean = 0, sd = sigma)
eps <- beta0 + beta1 * x + eps
y return(y)
}
Let’s create a basic list-like class so that we can define custom print, plot, and summary methods.
<- function(x, y, beta0, beta1, sigma) {
new_sim stopifnot(is.double(x),
is.double(y),
is.double(beta0),
is.double(beta1),
is.double(sigma),
>= 0)
sigma
return(
structure(list(x = x,
y = y,
beta0 = beta0,
beta1 = beta1,
sigma = sigma),
class = "sim")
) }
We can create a validator function to check that the specific structure is preserved
<- function(dat) {
validate_sim stopifnot(is.list(dat))
stopifnot(is.double(dat$x),
is.double(dat$y),
is.double(dat$beta0),
is.double(dat$beta1),
is.double(dat$sigma))
stopifnot(length(dat$x) == length(dat$y), dat$sigma >= 0)
return(dat)
}
Let’s re-write our function to return this S3 class.
<- function(x, beta0, beta1, sigma) {
simreg <- length(x)
n <- stats::rnorm(n = n, mean = 0, sd = sigma)
eps <- beta0 + beta1 * x + eps
y return(new_sim(x = x, y = y, beta0 = beta0, beta1 = beta1, sigma = sigma))
}
This will allow us to make new a plot method
<- function(x, y = NULL, ...) {
plot.sim ::qplot(x = x$x, y = x$y) +
ggplot2::geom_abline(slope = x$beta1,
ggplot2intercept = x$beta0,
lty = 2,
col = 2) +
::theme_bw() +
ggplot2::geom_smooth(method = "lm", se = FALSE) +
ggplot2::xlab("x") +
ggplot2::ylab("y")
ggplot2 }
We can also make a new summary method
<- function(object, ...) {
summary.sim <- lm(object$y ~ object$x)
lmout <- coef(lmout)[[1]]
beta0_ols <- coef(lmout)[[2]]
beta1_ols <- sigma(lmout)
sigma_ols return(
data.frame(parameter = c("beta0", "beta1", "sigma"),
truth = c(object$beta0, object$beta1, object$sigma),
ols = c(beta0_ols, beta1_ols, sigma_ols))
) }
Let’s try all of this out
<- simreg(x = runif(100), beta0 = 1, beta1 = 2, sigma = 0.25)
dat plot(dat)
## `geom_smooth()` using formula 'y ~ x'
summary(dat)
## parameter truth ols
## 1 beta0 1.00 0.9333
## 2 beta1 2.00 2.1661
## 3 sigma 0.25 0.2534
Generics, methods, constructors, validators, and helpers are all just regular functions, so you can document them as you would regular functions.
It is sometimes nice to have the same help file for the default method and the generic. You can do that via the @describeIn
{roxygen}
tag.
#' Generic Function for generic.
#'
#' @param x An R object.
<- function(x, ...) {
generic
}
#' @describeIn generic Default Method
#'
#' @param y is some default option
#'
<- function(x, y = NULL, ...) {
generic.default
}
See an example usage of this for the mean()
and summary()
documentation.
Exercise: Document your pop()
generic and the methods you made for pop()
.
Whenever you make a new S3 class, you should always provide a type predict to test if an object is a certain type.
<- function(x) {
is_sim return(inherits(x, "sim"))
}<- simreg(x = runif(100), beta0 = 1, beta1 = 2, sigma = 0.25)
dat is_sim(dat)
## [1] TRUE
is_sim(mtcars)
## [1] FALSE
Every variable in R has some implicit class even if it does not have a class
attribute.
This implicit class is used to define methods for these objects, and to control method dispatch when you use a base type on a generic.
sloop::s3_class()
will return the implicit or explicit class of all objects.
<- c(1, 2, 3)
x ::otype(x) ## not an S3 object sloop
## [1] "base"
::s3_class(x) ## implicit S3 class sloop
## [1] "double" "numeric"
<- matrix(1:6, nrow = 3, ncol = 2)
x ::otype(x) ## not an S3 object sloop
## [1] "base"
::s3_class(x) sloop
## [1] "matrix" "integer" "numeric"
So to create new matrix methods, you can do
<- function(...) {
generic.matrix
}
even though matrix
is not an S3 class.
The following functions are called “group generics” +
, -
, *
, /
, ^
, %%
, %/%
, &
, |
, !
, ==
, !=
, <
, <=
, >=
, and >
.
You can define methods for these group generics, but undergo what’s called double dispatch, choosing a method based on both arguments. This is what allows you to add integers and dates together. We will talk about how to do this correctly in the next lecture.
{vctrs}
packageBy far, the most common use for S3 objects are list-like objects to add plot()
/summary()
/print()
methods for folks who use your package.
But what if you want to create vector-like objects (e.g. Dates
and factors
)? Hadley provides a lot of nice examples:
It’s a lot of bookkeeping to do this properly. The {vctrs}
package makes it a lot easier.
Read the {vctrs}
vignette for more: https://vctrs.r-lib.org/articles/s3-vector.html
The simplest class, for percents, just changes the print method for doubles, but it is still a lot of work to get it to work:
percent
constructor
<- function(x = double()) {
new_percent stopifnot(is.double(x))
return(vctrs::new_vctr(x, class = "percent"))
}
percent
helper
<- function(x = double()) {
percent <- vctrs::vec_cast(x, double()) # tries to convert to double
x return(new_percent(x))
}
format()
method for percent
(in {vctrs}
this also controls the print()
method)
<- function(x, ...) {
format.percent <- formatC(vctrs::vec_data(x) * 100, digits = 1, format = "f")
ret is.na(x)] <- NA
ret[!is.na(x)] <- paste0(ret[!is.na(x)], "%")
ret[return(ret)
}
Allow percent
objects to be combined with doubles (using vec_c()
)
# type for combination should be percent
<- function(x, y, ...) {
vec_ptype2.percent.double percent()
}
<- function(x, y, ...) {
vec_ptype2.double.percent percent()
}
# How to cast (change type) based on operation
<- function(x, to, ...) percent(x)
vec_cast.percent.double <- function(x, to, ...) vctrs::vec_data(x) vec_cast.double.percent
<- percent(c(0.1, 0.2))
x ::vec_c(x, 0.1) vctrs
## <percent[3]>
## [1] 10.0% 20.0% 10.0%
Allow percents to be added/subtracted
<- function(op, x, y, ...) {
vec_arith.percent UseMethod("vec_arith.percent", y)
}<- function(op, x, y, ...) {
vec_arith.percent.default stop_incompatible_op(op, x, y)
}<- function(op, x, y, ...) { # method when have two percents
vec_arith.percent.percent switch(
op,"+" = , # go to next
"-" = new_percent(vctrs::vec_arith_base(op, x, y)), # do double ops, then convert
"/" = vctrs::vec_arith_base(op, x, y), # units cancel
stop_incompatible_op(op, x, y) # * makes less sense
) }
<- percent(c(0.1, 0.1))
x <- percent(c(0.5, 0.7))
y + y x
## <percent[2]>
## [1] 60.0% 80.0%
- y x
## <percent[2]>
## [1] -40.0% -60.0%
/ y x
## [1] 0.2000 0.1429
Most folks don’t need to create new vectors, so we won’t cover this package in more detail. But it is really cool what you can do with it.
class()
: Assign or get the class attribute.unclass()
: Remove class attribute and obtain underlying base type.inherits()
: Test if an object is an instance of a given class.sloop::ftype()
: See if a function is a “regular/primitive/internal function, a internal/S3/S4 generic, or a S3/S4/RC method”.sloop::s3_dispatch()
: View method dispatch.sloop::s3_methods_generic()
: View all methods of a generic function.sloop::s3_methods_class()
: View all methods implemented for a specific class.sloop::s3_class()
: Returns implicit and explicit class.sloop::is_s3_method()
: Predicate function for determining if a function is an S3 method.UseMethod()
: Used in a generic to define it as a generic.NextMethod()
: Apply the next method, in the method dispatch chain, of the called generic.