<- 1:10 # a numeric vector
x <- factor(c('a', 'b')) # a factor
y
c(is.object(x), is.object(y))
[1] FALSE TRUE
c(sloop::otype(x), sloop::otype(y))
[1] "base" "S3"
attr(x, 'class')
NULL
attr(y, 'class')
[1] "factor"
Notes on learning OOP in R, and trying to figure out the situation where I can benefit from using OOP.
Chi Zhang
February 23, 2023
Useful references:
Why use OOP: polymorphism - same function can be used on different types of input.
Without OOP, if someone wants to add new functionality to for example summary()
, he needs to ask the original author to change if-else statements inside the summary function. OOP allows any developer to extend the interface for new types of input.
mean()
of a vector of numbers is a number, mean()
of a vector of dates is a dateEncapsulated OOP:
object.method(arg1, arg2)
Functional OOP:
generic(object, arg2, arg3)
Check whether an object is object-oriented, or base object:
is.object()
sloop::otype()
: returns base or S3/S4attr(obj_name, 'class')
: OO objects has a class attribute, BO does not.[1] FALSE TRUE
[1] "base" "S3"
NULL
[1] "factor"
All objects have a base type; not all are OO objects.
typeof(1:10)
returns ‘integer’NULL, logical, integer, double, complex, character, list, raw
closure, special, builtin
environment
S4
symbol, language, pairlist
the rest are less common.Allows the function to return rich results with user-friendly display and programmer-friendly internals.
[1] "integer"
$levels
[1] "a" "b"
$class
[1] "factor"
Check whether a function is a generic: sloop::ftype()
The generic finds the method (implementation of print(), summary()
for a specific class) by performing method dispatch.
generic.class()
, for example: print.factor()
.
in the name; however it is not guaranteed * t.test()
is a generic like print()
, as t.test()
can be used on multiple types of inputs
as.factor()
is not an OO object, hence not S3sloop::ftype()
methods()
methods()
checks all the methods that either:
plot, predict, t.test
lm, ar
[1] predict.ar* predict.Arima*
[3] predict.arima0* predict.glm
[5] predict.HoltWinters* predict.lm
[7] predict.loess* predict.mlm*
[9] predict.nls* predict.poly*
[11] predict.ppr* predict.prcomp*
[13] predict.princomp* predict.smooth.spline*
[15] predict.smooth.spline.fit* predict.StructTS*
see '?methods' for accessing help and source code
[1] add1 alias anova case.names coerce
[6] confint cooks.distance deviance dfbeta dfbetas
[11] drop1 dummy.coef effects extractAIC family
[16] formula hatvalues influence initialize kappa
[21] labels logLik model.frame model.matrix nobs
[26] plot predict print proj qr
[31] residuals rstandard rstudent show simulate
[36] slotsFromS3 summary variable.names vcov
see '?methods' for accessing help and source code
Equivalently, use sloop::s3_methods_*()
, as it gives more information in the output.
# A tibble: 16 × 4
generic class visible source
<chr> <chr> <lgl> <chr>
1 predict ar FALSE registered S3method
2 predict Arima FALSE registered S3method
3 predict arima0 FALSE registered S3method
4 predict glm TRUE stats
5 predict HoltWinters FALSE registered S3method
6 predict lm TRUE stats
7 predict loess FALSE registered S3method
8 predict mlm FALSE registered S3method
9 predict nls FALSE registered S3method
10 predict poly FALSE registered S3method
11 predict ppr FALSE registered S3method
12 predict prcomp FALSE registered S3method
13 predict princomp FALSE registered S3method
14 predict smooth.spline FALSE registered S3method
15 predict smooth.spline.fit FALSE registered S3method
16 predict StructTS FALSE registered S3method
# A tibble: 35 × 4
generic class visible source
<chr> <chr> <lgl> <chr>
1 add1 lm FALSE registered S3method
2 alias lm FALSE registered S3method
3 anova lm FALSE registered S3method
4 case.names lm FALSE registered S3method
5 confint lm TRUE stats
6 cooks.distance lm FALSE registered S3method
7 deviance lm FALSE registered S3method
8 dfbeta lm FALSE registered S3method
9 dfbetas lm FALSE registered S3method
10 drop1 lm FALSE registered S3method
# ℹ 25 more rows
Two options: structure()
, or class(existing_obj)
Or, you can do it for an existing object by giving it a class
Examine what comes out
[name of method] <- functionn(x){UseMethod("[name of method]")}
Now we define one generic function f
, and two methods. One for class plus2
, and another for class plus10
.
Now we try to give the function some input. First use a numeric number, 1 (the class for a number is double
and numeric
).
Error in UseMethod("f"): no applicable method for 'f' applied to an object of class "c('double', 'numeric')"
This returns an error, because the class of number
is not defined for function f
(plus2
, plus10
).
f.double
f.numeric
f.default
We need to match it. Assign the number with plus2
class, and evaluate it. You can check which method has been used (dispatched).
[1] 3
attr(,"class")
[1] "plus2"
=> f.plus2
f.default
Now we try another number, but let it be plus10
class.
print()
)We create the S3 object using the constructor defined above, fruit()
.
$fruit_name
[1] "pineapple"
attr(,"class")
[1] "fruit"
The output does not look very nice, we can modify what prints out. Since print()
is an exisiting generic function, we do not need to define a new one (i.e. UseMethod
). We define the new method directly: generic.your_class
.