4 OO field guide
4.1 S3
Q: Read the source code for
t()
andt.test()
and confirm thatt.test()
is an S3 generic and not an S3 method. What happens if you create an object with classtest
and callt()
with it?
A: We can see thatt.test()
is a generic, because it callsUseMethod()
t.test #> function (x, ...) #> UseMethod("t.test") #> <bytecode: 0x556bb4a0c360> #> <environment: namespace:stats>
If we create an object with class
test
,t
will cause R to callt.test.default()
, unless you create a methodt.test()
for the generict()
.Q: What classes have a method for the
Math
group generic in base R? Read the source code. How do the methods work?
A:methods("Math") #> [1] Math,nonStructure-method Math,structure-method #> [3] Math.data.frame Math.Date #> [5] Math.difftime Math.factor #> [7] Math.POSIXt #> see '?methods' for accessing help and source code
Q: R has two classes for representing date time data,
POSIXct
andPOSIXlt
, which both inherit fromPOSIXt
. Which generics have different behaviours for the two classes? Which generics share the same behaviour?
A: Since both inherit from “POSIXt”, these should be the same for both classes:methods(class = "POSIXt") #> [1] - + all.equal as.character Axis #> [6] coerce cut diff hist initialize #> [11] is.numeric julian Math months Ops #> [16] pretty quantile quarters round seq #> [21] show slotsFromS3 str trunc weekdays #> see '?methods' for accessing help and source code
And these should be different (or only existing for one of the classes):
methods(class = "POSIXct") #> [1] [ [[ [<- as.data.frame as.Date #> [6] as.list as.POSIXlt c coerce format #> [11] initialize length<- mean print rep #> [16] show slotsFromS3 split summary Summary #> [21] weighted.mean xtfrm #> see '?methods' for accessing help and source code methods(class = "POSIXlt") #> [1] [ [[ [<- anyNA as.data.frame #> [6] as.Date as.double as.list as.matrix as.POSIXct #> [11] c coerce duplicated format initialize #> [16] is.na length length<- mean names #> [21] names<- print rep show slotsFromS3 #> [26] sort summary Summary unique weighted.mean #> [31] xtfrm #> see '?methods' for accessing help and source code
Q: Which base generic has the greatest number of defined methods?
A:library("methods") objs <- mget(ls("package:base"), inherits = TRUE) funs <- Filter(is.function, objs) generics <- Filter(function(x) ("generic" %in% pryr::ftype(x)), funs) sort( lengths(sapply(names(generics), function(x) methods(x), USE.NAMES = TRUE)), decreasing = TRUE )[1] #> print #> 207
Q:
UseMethod()
calls methods in a special way. Predict what the following code will return, then run it and read the help forUseMethod()
to figure out what’s going on. Write down the rules in the simplest form possible.y <- 1 g <- function(x) { y <- 2 UseMethod("g") } g.numeric <- function(x) y g(10) #> [1] 2 h <- function(x) { x <- 10 UseMethod("h") } h.character <- function(x) paste("char", x) h.numeric <- function(x) paste("num", x) h("a") #> [1] "char a"
A:
g(10)
will return2
. Since onlyx
is in the execution environment ofg.numeric
R will search fory
in the enclosing environment, wherey
is defined as2
.h("a")
will return"class a"
, becausex = "a"
is given as input to the called method. From?UseMethod
:UseMethod creates a new function call with arguments matched as they came in to the generic. Any local variables defined before the call to UseMethod are retained (unlike S).
So generics look at the class of their first argument (default) for method dispatch. Then a call to the particular method is made. Since the methods are created by the generic, R will look in the generics environment (including all objects defined before (!) the
UseMethod
statement) when an object is not found in the environment of the called method.h("a")
will return"char a"
, becausex = "a"
is given as input to the called method, which is of class character and soh.character
is called and R also doesn’t need to look elsewhere forx
.Q: Internal generics don’t dispatch on the implicit class of base types. Carefully read
?"internal generic"
to determine why the length off
andg
is different in the example below. What function helps distinguish between the behaviour off
andg
?f <- function() 1 g <- function() 2 class(g) <- "function" class(f) #> [1] "function" class(g) #> [1] "function" length.function <- function(x) "function" length(f) #> [1] 1 length(g) #> [1] "function"
A: From
?"internal generic"
:Many R objects have a class attribute, a character vector giving the names of the classes from which the object inherits. If the object does not have a class attribute, it has an implicit class, “matrix”, “array” or the result of mode(x) (except that integer vectors have implicit class “integer”). (Functions oldClass and oldClass<- get and set the attribute, which can also be done directly.)
length
does not find theclass
off
(“function”), so the methodlength.function
is not called. This is becausef
doesn’t have a class - which is needed for the S3 method dispatch of internal generics (those that are implemented in C, you can check if they are generics withpryr::ftype
) - only an implicit class. It is very confusing, becauseclass(f)
returns this (implicit) class.
You can check if a class is only implicit by using one of the following approaches:is.object(f)
returnsFALSE
oldClass(f)
returnsNULL
attributes(f)
doesn’t contain a$class
field
4.2 S4
Q: Which S4 generic has the most methods defined for it? Which S4 class has the most methods associated with it?
A:Generics:
We restrict our search to those packages that everyone should have installed:
search() #> [1] ".GlobalEnv" "package:stats" "package:graphics" #> [4] "package:grDevices" "package:utils" "package:datasets" #> [7] "package:methods" "Autoloads" "package:base"
Then we start our search for generics and keep those of otype S4:
generics <- getGenerics(where = search()) is_gen_s4 <- vapply(generics@.Data, function(x) pryr::otype(get(x)) == "S4", logical(1)) generics <- generics[is_gen_s4]
Finally we calculate the S4-generic with the most methods:
sort(sapply(generics, function(x) length(methods(x))), decreasing = TRUE)[1] #> coerce #> 27
Classes:
We collect all S4 classes within a character vector:
s4classes <- getClasses(where = .GlobalEnv, inherits = TRUE)
Then we are going to steal the following function from S4 system development in Bioconductor that returns all methods to a given class
s4Methods <- function(class){ methods <- showMethods(classes = class, printTo = FALSE) # notice the last setting methods <- methods[grep("^Function:", methods)] sapply(strsplit(methods, " "), "[", 2) }
Finally we apply this function to get the methods of each class and format a little bit to answer the question:
s4class_methods <- lapply(s4classes, s4Methods) names(s4class_methods) <- s4classes sort(lengths(s4class_methods), decreasing = TRUE)[1] #> ANY #> 110
Q: What happens if you define a new S4 class that doesn’t “contain” an existing class? (Hint: read about virtual classes in
?Classes
.)
A: Since?Classes
is deprecated we refer to?setClass
:Calls to setClass() will also create a virtual class, either when only the Class argument is supplied (no slots or superclasses) or when the contains= argument includes the special class name “VIRTUAL”.
In the latter case, a virtual class may include slots to provide some common behavior without fully defining the object—see the class traceable for an example. Note that “VIRTUAL” does not carry over to subclasses; a class that contains a virtual class is not itself automatically virtual.
Q: What happens if you pass an S4 object to an S3 generic? What happens if you pass an S3 object to an S4 generic? (Hint: read
?setOldClass
for the second case.)
A:
4.3 RC
Q: Use a field function to prevent the account balance from being directly manipulated. (Hint: create a “hidden”
.balance
field, and read the help for the fields argument insetRefClass()
.)
A: We are not that experienced in general RC classes, but it is easy with R6 classes. You can find all the information you need here. To solve the exercise this introduction should be sufficient:# definition of the class Account2 <- R6::R6Class("Account", public = list( initialize = function(balance = 0){ private$balance = balance }, withdraw = function(x){ if (private$balance < x) stop("Not enough money") private$balance <- private$balance - x }, deposit = function(x) { private$balance <- private$balance + x } ), private = list( balance = NULL ) ) # Checking the behaviour # a <- Account2$new(100) # a$withdraw(50); a # a$balance # a$balance <- 5000 # a$deposit(100); a # a$withdraw(200); a
Q: I claimed that there aren’t any RC classes in base R, but that was a bit of a simplification. Use
getClasses()
and find which classesextend()
fromenvRefClass
. What are the classes used for? (Hint: recall how to look up the documentation for a class.)
A: We get these classes as described in the exercise:classes <- getClasses(where = .GlobalEnv, inherits = TRUE) classes[unlist(lapply(classes, function(x) methods::extends(x, "envRefClass")))] #> [1] "envRefClass" "refGeneratorSlot" "localRefClass"
Their need is best described in
class?envRefClass
“Purpose of the Class”:This class implements basic reference-style semantics for R objects. Objects normally do not come directly from this class, but from subclasses defined by a call to setRefClass. The documentation below is technical background describing the implementation, but applications should use the interface documented under setRefClass, in particular the $ operator and field accessor functions as described there.