OOP_R

Note

Cursorily getting to speed with Object Oriented Programming in R for an upcoming project. Added a little prose of my own while following the vignettes:

1 Getting started with R7

So R7 is a new OOP system in R that revamps S3 and S4 and tries to build on their strengths.

Let’s learn the basics 🙂.

1.1 Classes and objects

From Wikipedia

Note

Object-oriented programming (OOP) is a programming paradigm based on the concept of “objects”, which can contain data and code:

  • data in the form of fields (often known as attributes or properties),

  • code, in the form of procedures (often known as methods). - functions?

In object-oriented programming, a class is an extensible program-code-template for creating objects, providing initial values for state (member variables) and implementations of behavior (member functions or methods).

R7 class definition include a list of properties (data) that the object will possess and an optional validator.

Defined using new_class()

library(R7)
# Define a new class called range
range <- new_class(name = "range",
                   # Specify properties/data
                   properties = list(
                     start = class_double,
                     end = class_double
                   ),
                   # Validator to check whether object is valid
                   validator = function(self){
                     if (length(self@start) != 1){
                       "@start must be length 1"
                     } else if (length(self@end) != 1){
                       "@end must be length 1"
                     } else if (self@end < self@start)
                       "@end must be greater than or equal to @start"
                   }
                   )

new_class() returns a class object - a function that can be used to create object of the given class

# Create an object/instance of the class?
x <- range(start = 1, end = 10)
x
<range>
 @ start: num 1
 @ end  : num 10

1.1.1 Properties/Attributes

Properties/attributes refer to the data possessed by an object. We use @ to set and get the properties (similar to . in Python?)

# Get attributes
x@start
[1] 1
# Modify attributes
x@end <-  20
x
<range>
 @ start: num 1
 @ end  : num 20

The object’s property are validated by the class validator and the type declared when defining the class’ properties:

# Enter an integer instead of a double as one of the inputs
x@start <- 1L
#> Error: <range>@start must be <double>, not <integer>

# Make end less than start
x@end <- -10
#> - @end must be greater than or equal to @start

1.2 Generic and methods

R7 uses functional OOP where methods belong to generic functions and method calls take the form: generic(object, arg2, arg3)

A generic defines the interface of a function. From Advanced R:

Note

The generic is a middleman: its job is to define the interface (i.e. the arguments) then find the right implementation for the job. The implementation for a specific class is called a method, and the generic finds that method by performing method dispatch.

So let’s create a generic using new_generic()

# Define a generic where subsequent methods will belong to
inside <- new_generic(name = "inside",
    # A character vector giving the names of one or more arguments used to find the method
                      dispatch_args = "x",
    function(x, y){
      # Actually finds and calls the appropriate method
      R7_dispatch()
    })

Once you have a generic, you provide implementations for specific classes by registering methods with method<-:

# Add a method to the generic/class?
# Method belonging to inside will be implement a function(x, y) to class range
method(inside, range) <- function(x, y){
  y >= x@start & y <= x@end
  # This means x is a range object?
}

# Print generic
inside
<R7_generic> inside(x, y) with 1 methods:
1: method(inside, range)
# Make a method call: generic(object, args...)
inside(x, c(0, 5, 10, 15))
[1] FALSE  TRUE  TRUE  TRUE

You can use method<- to define an implementations for base types on R7 generics

# Method belonging to generic will implement comparison function
# Add method to base type
method(inside, class_numeric) <- function(x, y){
  y >= min(x) & y <= max(x)
  
}

# Print generic
inside
<R7_generic> inside(x, y) with 3 methods:
1: method(inside, range)
2: method(inside, class_integer)
3: method(inside, class_double)
# Make a method call
v <- 1:5
inside(v, 3:8)
[1]  TRUE  TRUE  TRUE FALSE FALSE FALSE

One can also register methods for R7 classes on S3 and S4 generics:

# Register a mean method for class range on S3/S4 generic mean
method(mean, range) <- function(x, ...){
  (x@start + x@end) / 2
}

mean(x)
[1] 10.5

Let’s try adding a print method

method(print, range) <- function(x, ...){
  cat("Value(data = [", x@start, x@end, "])")
}

y <- range(start = 3, end = 6)
y
Tip

First, you should only ever write a method if you own the generic or the class. R will allow you to define a method even if you don’t, but it is exceedingly bad manners. Instead, work with the author of either the generic or the class to add the method in their code ~ Advanced R

2 PART 2

A look at the vignette.

Define a simple pet class with two properties/attributes: name and age

dog <- new_class("dog",
                 properties = list(
                   name = class_character,
                   age = class_numeric
                 ))

dog
<R7_class>
@ name  :  dog
@ parent: <R7_object>
@ properties:
 $ name: <character>          
 $ age : <integer> or <double>

Now create an instance of the class/object

lola <- dog(name = "Lola", age = 11)
lola
<dog>
 @ name: chr "Lola"
 @ age : num 11

Creating generics: an interface which uses a different implementation depending on the class or more arguments

# Create a generic for imlementing methods for dog class
speak <- new_generic("speak", "x")
speak
<R7_generic> speak(x, ...) with 0 methods:

Once you have a generic, you can register methods for specific classes using method(generic, class) <- implementation

# Register a method for class dog
method(speak, dog) <- function(x){
  "Woof"
}

speak
<R7_generic> speak(x, ...) with 1 methods:
1: method(speak, dog)

Once a method is registered, the generic will use it when appropriate

speak(lola)
[1] "Woof"

Define another class, this one for cats and define another method for speak

# Define a class cat
cat <- new_class("class",
                 properties = list(
                   name = class_character,
                   age = class_numeric
                 ))

# Register a method for class cat in geneic speak
method(speak, cat) <- function(x){
  "Meow"
}

# Create a cat object
jessie <- cat(name = "Jessie", age = 6)

speak(jessie)
[1] "Meow"

2.1 Method dispatch and inheritance

cat and dog classes share similar attributes hence we could use a common parent class

# Create pet class
pet <- new_class("pet",
                 properties = list(
                   name = class_character,
                   age = class_numeric
                 ))

# Inherit properties
dog <- new_class("dog", parent = pet)
cat <- new_class("cat", parent = pet)

# Create new objects
jessie <- cat(name = "Jessie", age = 6)
lola <- dog(name = "Lola", age = 11)

Method dispatch takes advantage of the parent classes: if the method is not defined for a class, it will try the method for the parent class, and so on until it finds a method

# Create a eneric for parent class
describe <- new_generic("describe", "x")

# Register a method for pet class
method(describe, pet) <- function(x){
  paste0(x@name, " is ", x@age, " years old")}
  
  
describe(lola)
[1] "Lola is 11 years old"
# Register a method for cat class exclusively
method(describe, cat) <- function(x){
  paste0(x@name, " is a ", x@age, " year old good cat")
}

describe(jessie)
[1] "Jessie is a 6 year old good cat"

Define a fallback method for any R7 object by registering a method for R7_object

method(describe, R7_object) <- function(x){
  "An R7 object"
}

2.2 Default value

SOmetimes its great to have defaults in your classes

empty <- new_class("empty",
                   properties = list(
                     x = new_property(class_numeric, default = 0),
                     y = new_property(class_character, default = ""),
                     z = new_property(class_logical, default = NA)
                   ))

empty()
<empty>
 @ x: num 0
 @ y: chr ""
 @ z: logi NA

2.3 Computed properties

Sometimes its useful to have a property computed on demand. This can be achieved by defining a getter

range <- new_class("range",
                   properties = list(
                     start = class_double,
                     end = class_double,
                     length = new_property(
                       getter = function(self) self@end - self@start
                     )
                     
                   ))

x <- range(1, 10)
x
<range>
 @ start : num 1
 @ end   : num 10
 @ length: num 9

Computed properties are read-only

x@length <- 20
#> Error: Can't set read-only property <range>@length

2.4 Dynamic properties

You can make a computed property fully dynamic so that it can be read and written by also supplying a setter. The previous example can be extended to allow @length to be set, by modifying the @end of the vector:

# Create class
range <- new_class("range",
               properties = list(
                 start = class_double,
                 end = class_double,
                 length = new_property(
                   class = class_double,
                   setter = function(self, value){
                     self@end <- self@start + value
                     self},
                   getter = function(self) self@end - self@start
                 )
               ))


# Create instance of class
x <- range(start = 1, end = 10)
x
<range>
 @ start : num 1
 @ end   : num 10
 @ length: num 9
# Modiy length
x@length <- 5
x
<range>
 @ start : num 1
 @ end   : num 6
 @ length: num 5

A setter returns a modified object.

setter/getter -used for computed/dynamic properties?