library(distr6)
set.seed(42)
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")

OOP and Design Patterns

OOP Practices and Definitions

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

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) [@GoF].

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.

R6 and OOP

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:

  1. Abstract Classes
  2. Decorators
  3. Adapters

Abstract Classes

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.

Decorators

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"))
})

Adapters

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)
})

References



RaphaelS1/distr6 documentation built on Feb. 24, 2024, 9:14 p.m.