library(R7)
# Define a new class called range
<- new_class(name = "range",
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"
} )
OOP_R
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
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()
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?
<- range(start = 1, end = 10)
x 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
@start x
[1] 1
# Modify attributes
@end <- 20
x 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
@start <- 1L
x#> Error: <range>@start must be <double>, not <integer>
# Make end less than start
@end <- -10
x#> - @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
:
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
<- new_generic(name = "inside",
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){
>= x@start & y <= x@end
y # 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){
>= min(x) & y <= max(x)
y
}
# 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
<- 1:5
v 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, ...){
@start + x@end) / 2
(x
}
mean(x)
[1] 10.5
Let’s try adding a print method
method(print, range) <- function(x, ...){
cat("Value(data = [", x@start, x@end, "])")
}
<- range(start = 3, end = 6)
y y
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
<- new_class("dog",
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
<- dog(name = "Lola", age = 11)
lola 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
<- new_generic("speak", "x")
speak 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
<- new_class("class",
cat 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
<- cat(name = "Jessie", age = 6)
jessie
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
<- new_class("pet",
pet properties = list(
name = class_character,
age = class_numeric
))
# Inherit properties
<- new_class("dog", parent = pet)
dog <- new_class("cat", parent = pet)
cat
# Create new objects
<- cat(name = "Jessie", age = 6)
jessie <- dog(name = "Lola", age = 11) lola
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
<- new_generic("describe", "x")
describe
# 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
<- new_class("empty",
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
<- new_class("range",
range properties = list(
start = class_double,
end = class_double,
length = new_property(
getter = function(self) self@end - self@start
)
))
<- range(1, 10)
x x
<range>
@ start : num 1
@ end : num 10
@ length: num 9
Computed properties are read-only
@length <- 20
x#> 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
<- new_class("range",
range properties = list(
start = class_double,
end = class_double,
length = new_property(
class = class_double,
setter = function(self, value){
@end <- self@start + value
self
self},getter = function(self) self@end - self@start
)
))
# Create instance of class
<- range(start = 1, end = 10)
x x
<range>
@ start : num 1
@ end : num 10
@ length: num 9
# Modiy length
@length <- 5
x x
<range>
@ start : num 1
@ end : num 6
@ length: num 5
A setter returns a modified object.
setter/getter
-used for computed/dynamic properties?