vignettes/webs/r6_and_design_patterns.rmd
r6_and_design_patterns.rmd
Instead of providing a comprehensive glossary of OOP terms (of which many exist) we instead give a very brief overview of the most common OOP practices found in R.
Firstly, it should be noted that R is a functional language that makes use of dispatch and is not primarily for OOP. Functional programming and dispatch lends it’s hand very naturally to the Strategy and Visitor design patterns (which we will return to later), the first of R’s OOP style ‘sub-languages’, S3, should not be taken for granted therefore as it provides very powerful workarounds to strict OOP methods.
R’s update to S3, S4, formally introduces the fundamentals of OOP: encapsulation, abstraction and inheritance. Combined abstraction and encapsulation refer to only giving the user access to methods and data that they require and hiding everything else, it is the principle of minimising the user-interface (UI) and keeping as much uniformity and efficiency as possible. Inheritance is the process of one class (the child-class) ‘copying’ methods and variables from another (the parent-class).
R6 formalises these methods further by clearly defining the notion of
a class and creating methods to construct the class (and thereby
creating an object). R6 also introduces notions of method chaining and
cloning, to allow a chain of methods to be called and removing the need
to re-create and duplicate an object each time). More concretely, if a
user wants to add the variable y
to x
and save
the result then they would call x = x + y
but if
x
is an R6 object then the user simply calls
x + y
.
Finally, an abstract class is defined as a class that cannot be constructed, i.e. an object or instance of the class cannot be created. The purpose of an abstract class is to have multiple child classes all inherit common methods/variables.
Design Patterns were collated, formalised and introduced in the seminal Design Patterns book (Gamma et al.) and the authors are commonly referred to as the Gang of Four (GoF) (Gamma et al. 1994).
By far the most common design patterns in R toolboxes are the strategy and visitor design patterns. From GoF:
Both of these patterns can be achieved via single or multiple dispatch and the S3 generic system is essentially a work-around for both of them. In many toolboxes, the concept of ‘wrappers’ and ‘composites’ are discussed. This is especially confusing as ‘wrapper’ may refer to one of two design patterns and ‘composite’ is a pattern in itself. The term wrapper usually refers to either the decorator or adapter design pattern, again from GoF:
The key difference is that adapters change the class interface, decorators add methods to the interface and composites allow individuals and their composites to be treated the same.
Despite R6 becoming more commonplace in R packages, we have found no documentation of best practices for using R6 and OOP methods and design patterns. Hence we propose our own R6 snippets and workarounds for common design patterns and other OOP methods. We have implemented the following OOP processes and design patterns thus far:
Abstract classes are classes that cannot be instantiated. They are useful for defining hierarchical structures and inheritance in OOP, as well as for the abstract factory design pattern.
Implementation: In R6, all classes are concrete and by default have
an initialize
method for construction. Therefore, to make a
class abstract we overload the initialize
method as
follows:
> AbstractClass$set("public","initialize",function(){
+ stop(paste(RSmisc::getR6Class(self), "is an abstract class that can't be initialized."))
+ })
# So on construction
> AbstractClass$new()
Error in .subset2(public_bind_env, "initialize")(...) :
AbstractClass is an abstract class that can't be initialized.
From Gamma et al.:
“(Decorators) Attach additional responsibilities to an objects dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.”
Implementation: Decorators are particularly complex in R6 for a number of reasons. Firstly, inheritance occurs in the class definition and not object definition, therefore we cannot dynamically choose which class to inherit from. Secondly, methods and variables should only be defined before initializing an object and any defined after do not have access to the object itself (i.e. the ‘self’ and ‘private’ accessors). And finally there is no simple way to reference one object dynamically from another without this being hardcoded. Implementation has two key methods, the first via construction of the class to be decorated and the second via construction of the decorator assuming an object has already been instantiated to decorate it.
Method 1) In the class to decorate we copy every
public method from the decorators of interest to the class and we add
self
as an argument to ensure that the decorator methods
have the same access level as ‘standard’ methods.
if(!is.null(decorators)){
lapply(decorators,function(x){
methods <- c(x$public_methods, get(paste0(x$inherit))$public_methods) # Combines decorator methods and any parent methods
methods <- methods[!(names(methods) %in% c("initialize","clone"))] # Ensures initialize and clone aren't copied
aself <- self
for(i in 1:length(methods)){
formals(methods[[i]] ) = c(formals(methods[[i]]),list(self=aself)) # Adds self as default to every decorator ensuring access to the object
assign(names(methods)[[i]],methods[[i]],envir=as.environment(self)) # Copies every method from the decorator to the object
}
})
}
private$.decorators = unlist(lapply(decorators,function(x) x[["classname"]]))
Here we assume decorators
is an argument to the
constructor given as a list naming the decorator classes and that a
private variable called .decorators
is a list of decorators
already present in the object.
Method 2) On construction of a particular decorator, the original object is overwritten with whichever decorators were already added and the new decorator that is being constructed. The decorator object is not saved to local memory.
DistributionDecorator$set("public","initialize",function(distribution){
if(getR6Class(self) == "DistributionDecorator")
stop(paste(getR6Class(self), "is an abstract class that can't be initialized.")) # Defines the Abstract Decorator parent class as abstract.
decorators = distribution$decorators() # Gets decorator list from object.
if(!is.null(decorators)){
decorators = lapply(decorators,get)
}
decorators = unique(c(decorators,get(getR6Class(self)))) # Combines decorators present in the object with the current decorator to be added.
assign(paste0(substitute(distribution)), Distribution$new(distribution)), pos = .GlobalEnv) # Constructs a new object via Method 1) and assigns this to the environment with the same name as the undecorated object.
cat(paste(substitute(distribution),"is now decorated with",getR6Class(self),"\n"))
})
For the wrappers in distr6, our code is closer to that of an adapter
than decorator pattern as we adapt the interface of an object and in
fact the object class is changed in the process. The implementation of
these in distr6 is quite simple, wrappers are classes inheriting from
Distribution
objects that have an additional method that
allows the user to view the internally wrapped models (or
distributions). Each wrapper has two parts to its constructor: the
parent-class method that ensures all parameters are unique, and the
child class method that makes any other changes to the object (usually
by editing its pdf and/or cdf)
Parent Class Constructor
DistributionWrapper$set("public","initialize",function(distlist, prefixParams = TRUE,...){
if(getR6Class(self) == "DistributionWrapper")
stop(paste(getR6Class(self), "is an abstract class that can't be initialized."))
assertDistributionList(distlist)
lapply(distlist, function(x) x$parameters()$update())
private$.wrappedModels <- distlist
if(prefixParams){
params <- data.table::rbindlist(lapply(distlist, function(x){
params = x[["parameters"]]()$as.data.table()
params[,1] = paste(x[["short_name"]],unlist(params[,1]),sep="_")
return(params)
}))
row.names(params) <- NULL
params <- as.ParameterSet(params)
} else{
if(length(distlist) == 1)
params <- distlist[[1]]$parameters()
else
params <- do.call(rbind,lapply(distlist, function(x) x$parameters()))
}
super$initialize(parameters = params, ...)
})
Abridged Child Class Constructor (TruncatedDistribution)
TruncatedDistribution$set("public","initialize",function(distribution, lower = NULL,
upper = NULL){
pdf <- function(x1,...) {
if(x1 <= self$inf() | x1 > self$sup())
return(0)
else
self$wrappedModels()[[1]]$pdf(x1) / (self$wrappedModels()[[1]]$cdf(self$sup()) - self$wrappedModels()[[1]]$cdf(self$inf()))
}
formals(pdf)$self <- self
cdf <- function(x1,...){
num = self$wrappedModels()[[1]]$cdf(x1) - self$wrappedModels()[[1]]$cdf(self$inf())
den = self$wrappedModels()[[1]]$cdf(self$sup()) - self$wrappedModels()[[1]]$cdf(self$inf())
return(num/den)
}
formals(cdf)$self <- self
name = paste("Truncated",distribution$name)
short_name = paste0("Truncated",distribution$short_name)
distlist = list(distribution)
description = paste0(distribution$description, " Truncated between ",lower," and ",upper,".")
super$initialize(distlist = distlist, pdf = pdf, cdf = cdf, name = name,
short_name = short_name, support = support,
type = distribution$type(), prefixParams = FALSE,
description = description)
})