Another object orientation system in R
Another implementation of object-orientation in R. It provides syntactic sugar for the S4 class system and two alternative new implementations. One is an experimental version built around S4 and the other one makes it more convenient to work with lists as objects.
Installation
To install from CRAN:
install.packages("aoos")
To install from this repo:
library(devtools)
install_github("wahani/aoos")
Examples:
retList:
Basically you define constructor functions. There is no formal class
definition. The function body will define what members an object will have. You
quit the function defining the return value using retList
which is a generic
constructor function. By default it will look at the environment from which it
is called and convert that environment into a list. That list is returned and is
an object. Names with a "." are not part of the constructed list (by default).
library("aoos")
Employee <- function(.name, .salary) {
"Common base class for all employees"
print <- function(x, ...) {
cat("Name : ", .self$.name, "\nSalary: ", .self$.salary)
}
getName <- function() .name
getSalary <- function() .self$.salary
retList(c("Employee", "Print"))
}
peter <- Employee("Peter", 5)
peter
## Name : Peter
## Salary: 5
peter$getName()
## [1] "Peter"
peter$getSalary()
## [1] 5
Here every instance is of class Employee and also inherits from class Print. This enables us to define the print method in the functions body and is equivalent to invoking the print method directly:
peter
## Name : Peter
## Salary: 5
peter$print()
## Name : Peter
## Salary: 5
retList + Inheritance:
You can inherit methods and fields from a super class, or rather an instance, because there is no formal class definition. Methods and fields can be replaced in the child, all member from the parent are also available for the methods of the child.
Manager <- function(.name, .salary, .bonus) {
"Extending the Employee class"
bonus <- function(x) {
if (!missing(x)) .self$.bonus <- x
.self$.bonus
}
print <- function(x, ...) {
cat("Name : ", .self$.name, "\nSalary: ", .self$.salary,
"\nBonus:", .self$.bonus)
}
retList("Manager", super = Employee(.name, .salary))
}
julia <- Manager("Julia", 5, 5 * 1e6)
julia
## Name : Julia
## Salary: 5
## Bonus: 5e+06
julia$getSalary()
## [1] 5
julia$bonus(10)
## [1] 10
julia
## Name : Julia
## Salary: 5
## Bonus: 10
S4 generics
As of version 0.3.1 there exist two binary operators, %g%
and %m%
, which
link to the S4 system for generic functions. They provide (hopefully) concise
alternatives to methods::setGeneric
and methods::setMethod
:
# Standard definition for a generic without default:
numeric : strLength(x, ...) %g% standardGeneric("strLength")
# A method for x:character
strLength(x ~ character, ...) %m% { nchar(x) }
# Kind of the default method for x:ANY
strLength(x ~ ANY, ...) %m% { strLength(as.character(x)) }
# Check that it works:
strLength(123)
## [1] 3
strLength("ab")
## [1] 2
You may have noticed that we also constrained the return value of any method
belonging to the generic strLength
to be a numeric. There exist methods
for objects of type character and ANY type.
In S4, methods can have defaults for arguments which are not formals of the
generic. Otherwise the defaults of the generic are passed down to its methods.
This is not changed: Define defaults for the generic. If a method has more
arguments than its generic you can define defaults for them. For the shared
argument names provide a class name. One exception to the rule is ...
which
can have no type.
S4 Types
The following presents the function %type%
which is a link to S4s setClass
.
%type%
tries to abstract a typical scenario of using setClass
.
In the following We define two types. One is Test which has two fields, x
and y. x is of type numeric, y is a list. Notice that you can either
define a prototype (a default) for a field (for which the class is inferred), or
you state the class explicitly using ~
.
The second is Child and inherits the properties from Test. Thus it has also two fields, x and y, and in addition we say it inherits from type character. So Child is basically a character vector with two attributes:
Test(x ~ numeric, y = list()) %type% {
stopifnot(length(.Object@x) == 1)
stopifnot(.Object@x > 0)
.Object
}
Test : character : Child() %type% .Object
Test(2)
## An object of class "Test"
## Slot "x":
## [1] 2
##
## Slot "y":
## list()
Child("Hej", x = 2)
## An object of class "Child"
## [1] "Hej"
## Slot "x":
## [1] 2
##
## Slot "y":
## list()
Notice that the right hand side of the expression is more or less the definition
of the initialization method for a type. Arbitrary operations can be made during
init, in the above example we formulate some assertions (x > 0 and scalar). The
init method for type Child just returns the object itself named .Object
(see the help page for methods::initialize
to understand the naming).
S4 Type Unions
S4 provides also the possibility to construct type unions which are useful to allow a type to inherit from different types at the same time, e.g. a type which can either be a numeric or character. This feature is not yet complete, but here are some ways you can use it. For the definition of a type:
'numeric | character' : Either() %type% .Object
Either(1)
## An object of class "Either"
## [1] 1
Either("Hello World!")
## An object of class "Either"
## [1] "Hello World!"
In the definition of a field:
Either(x ~ numeric | character) %type% .Object
Either(1)
## An object of class "Either"
## Slot "x":
## [1] 1
Either("Hello World!")
## An object of class "Either"
## Slot "x":
## [1] "Hello World!"
In the definition of a generic or method:
'numeric | character' : complicatedFunction(x = 1) %g% as.character(x)
complicatedFunction(x ~ character | integer) %m% as.numeric(x)
complicatedFunction()
## [1] "1"
complicatedFunction("1")
## [1] 1
complicatedFunction(1L)
## [1] 1
Polymorphic Methods in Objects
In contrast to the defaults in S4, %g%
and %m%
have side effects in the
environment they are called in. That means you can define generics which are
local to a function or closure. Nice all by itself but it also extends the
retList
-idea of representing objects in R as demonstrated here:
Class <- function() {
overloaded(x) %g% {
cat("This is the default ... \n")
x
}
overloaded(x ~ numeric) %m% {
cat("This is the method for 'numeric' values ... \n")
x
}
retList("Class")
}
instance <- Class()
instance$overloaded(1)
## This is the method for 'numeric' values ...
## [1] 1
instance$overloaded("a")
## This is the default ...
## [1] "a"
The next question is how to inherit or extend an existing generic which is a member of a class? I am not entirely happy with how this works at the moment, but this is one way to approach it (which works...):
Child <- function() {
# Normally you would make the call to the parents constructor in the call
# to retList. But here we need to access the elements directly during init...
.super <- Class()
# This points %m% to the generic (in .super) which should be extended:
.super$overloaded(x ~ integer) %m% {
cat("This is the method for 'integer' values ... \n")
x
}
retList("Child", super = .super)
}
instance <- Child()
instance$overloaded(1)
## This is the method for 'numeric' values ...
## [1] 1
instance$overloaded("a")
## This is the default ...
## [1] "a"
instance$overloaded(1L)
## This is the method for 'integer' values ...
## [1] 1
More unsorted ideas
Public Fields
Something you have to keep in mind is that returned objects are of class list. If you want to have a public field you have to define get and set methods, because you will see a copy of those fields in the object, they behave more like an attribute.
ObjectWithField <- function(name) {
getName <- function() {
name
}
retList()
}
obj <- ObjectWithField("Alexander")
obj$name <- "Noah"
obj$getName()
## [1] "Alexander"
obj$name
## [1] "Noah"
We can do more abstract representations of things. In this example I want to create a constructor object which keeps track of how many instances it created. Also every instance should know how many siblings it has, or in other words all instances share a reference to a field accessible by all of them.
initFamily <- function(.familyName) {
.superEnv <- environment()
.count <- 0
getCount <- function() {
cat("There are", .self$.count, paste0(.familyName, "s"), "out there.")
}
new <- function(.name) {
# happens on init
.count <<- .count + 1
print <- function(x, ...) cat("My name is", .self$.name, .familyName, "!")
countSiblings <- function() cat("I have", .count, "siblings!")
# So every instance knows about .count and .familyName:
retList(c("Person", "Print"), superEnv = new.env(parent = .superEnv))
}
retList("ConstructorPerson")
}
schmidt <- initFamily("Schmidt")
schmidt$getCount()
## There are 0 Schmidts out there.
lisa <- schmidt$new("Lisa")
sandra <- schmidt$new("Sandra")
schmidt$getCount()
## There are 2 Schmidts out there.
sandra$countSiblings()
## I have 2 siblings!
sandra
## My name is Sandra Schmidt !
lisa
## My name is Lisa Schmidt !
Self-contained Method Invocation
These ideas can be combined to construct something object like in S4 with fields as slots and methods contained in a list (other patterns can be thought of...):
# Some extra stuff to make the instance on which a method is invoked available
# during the call.
setClass("Self", contains = "VIRTUAL")
setMethod("$", "Self", function(x, name) {
function(...) x[[name]](.Object = x, ...)
})
## [1] "$"
Self : Show : Test : list : Super() %type% {
method <- function(.Object) .Object@x * 2
show <- function(.Object) cat("Value for x:", .Object@x)
S3Part(.Object) <- retList(super = .Object)
.Object
}
x <- Super(x = 2)
x
## Value for x: 2
x$method()
## [1] 4
And for the S3 retList thing:
"$.Self" <- function(x, name) {
callIsLocal <- any(sapply(aoos:::envirSearch(list(parent.frame())),
identical, y = attr(x, ".self")))
if (callIsLocal) {
obj <- attr(x, ".self")[[name]]
if (inherits(obj, "function")) function(...) obj(self = x, ...)
else obj
}
else function(...) x[[name]](self = x, ...)
}
"$<-.Self" <- function(x, name, value) {
callIsLocal <- any(sapply(aoos:::envirSearch(list(parent.frame())),
identical, y = attr(x, ".self")))
if (callIsLocal) {
assign(name, value = value, envir = attr(x, ".self"))
} else {
stop("This method has been disabled for objects of class 'Self'.")
}
x
}
Self <- function(.x) {
setX <- function(self, val) {
self$.x <- val
invisible(self)
}
getX <- function(self) {
self$.x
}
retList(c("Self"))
}
inst <- Self(2)
inst$setX(3)$setX(4)
inst$getX()
## [1] 4
inst$.x <- 2
## Error in `$<-.Self`(`*tmp*`, ".x", value = 2): This method has been disabled for objects of class 'Self'.
Syntactic Sugar for Return List
Person <- function(.name) {
print <- function(x, ...) {
cat(paste0("Hello, my name is ", .name, ".\n"))
}
retList(c("Person", "Print"))
}
Employee <- function(.id, ...) {
print <- function(x, ...) {
cat("Name: ", .name, "\nID: ", .id)
}
retList("Employee") %inherit% Person(...)
}
"%inherit%" <- function(child, parent) {
retList(class(child), names(child), super = parent, mergeFun = envCopy, list2env(child))
}
kalle <- Employee("1", "Kalle")
str(kalle)
## List of 1
## $ print:function (x, ...)
## ..- attr(*, "srcref")=Class 'srcref' atomic [1:8] 13 12 15 3 12 3 13 15
## .. .. ..- attr(*, "srcfile")=Classes 'srcfilecopy', 'srcfile' <environment: 0x6c68c20>
## - attr(*, ".self")=<environment: 0x668d730>
## - attr(*, "class")= chr [1:5] "Employee" "list" "Person" "Print" ...
class(kalle)
## [1] "Employee" "list" "Person" "Print" "list"