Q7 is expected to be compared with R6, the premier object facility in R. Q7 covers the majority of R6 capabilities. The main difference is that Q7 promotes compositional object construction, instead of hereditary.
The blueprint for an object: - R6: a class - Q7: a type
The object which creates instances from the blueprint - R6: a
generator - within, a $new()
method - Q7: a constructor
function
Functions defined inside an object - R6: a method - Q7: - a bound function (as opposed to a free function) - a domestic function (as opposed to a foreign function)
The following is the equivalent to examples from R6’s Introduction, leaving out original comments & explainations.
You can compare the the implementation of R6 and Q7 side-by-side.
Person <- type(function(name, hair){
name <- name
hair <- hair
set_hair <- function(val){
hair <<- val
}
greet <- function(){
cat(paste0("Hello, my name is ", name, ".\n"))
}
}, "Person")
Person
#> <Q7type:Person>
#> <environment: R_GlobalEnv>
Queue <- type(function(...){
private[queue] <- list()
private[length] <- function(){
base::length(queue)
}
add <- function(x){
queue <<- c(queue, list(x))
invisible(.my)
}
remove <- function() {
if (length() == 0) return(NULL)
head <- queue[[1]]
queue <<- queue[-1]
head
}
private[dots] <- list(...)
# this is necessary because ... (dot-dot-dot) must be captured here, and that
# the initialize() function must not take any arguments.
private[initialize] <- function(){
for (item in dots) {
add(item)
}
}
})
q <- Queue(5, 6, "foo")
Numbers <- type(function(){
x <- 100
active[x2] <- function(value) {
if (missing(value)) return(x * 2)
else x <<- value/2
}
active[rand] <- function(){
rnorm(1)
}
}, "Numbers")
n <- Numbers()
n$x
#> [1] 100
n$x2
#> [1] 200
n$x2 <- 1000
n$x
#> [1] 500
n$rand
#> [1] -0.4146329
n$rand
#> [1] -0.3988738
n$rand <- 3
#> Error in (function () : unused argument (base::quote(3))
HistoryQueue <- Queue %>%
implement({
head_idx <- 0
show <- function() {
cat("Next item is at index", head_idx + 1, "\n")
for (i in seq_along(queue)) {
cat(i, ": ", queue[[i]], "\n", sep = "")
}
}
remove <- function() {
if (length() - head_idx == 0) return(NULL)
head_idx <<- head_idx + 1
queue[[head_idx]]
}
})
hq <- HistoryQueue(5, 6, "foo")
hq$show()
#> Next item is at index 1
#> 1: 5
#> 2: 6
#> 3: foo
hq$remove()
#> [1] 5
hq$show()
#> Next item is at index 2
#> 1: 5
#> 2: 6
#> 3: foo
hq$remove()
#> [1] 6
NOTE: There is no inheritance in Q7, so you cannot call methods of your parent class. But you can rename anything you don’t meant to override.
CountingQueue <- Queue %>% implement({
private[total] <- 0
private[proto.add] <- add
add <- function(x) {
total <<- total + 1
proto.add(x)
}
get_total <- function() total
})
cq <- CountingQueue("x", "y")
cq$get_total()
#> [1] 2
cq$add("z")
cq$remove()
#> [1] "x"
cq$remove()
#> [1] "y"
cq$get_total()
#> [1] 3
SimpleClass <- type(function(){
x <- NULL
}, "SimpleClass")
SharedField <- type(function(){
e <- SimpleClass()
}, "SharedField")
s1 <- SharedField()
s1$e$x <- 1
s2 <- SharedField()
s2$e$x <- 2
s1$e$x
#> [1] 1
Q7 and R6 again show differnet behavior. In Q7’s case,
s1
’s x
isn’t changed with that of
s2
. The x
in the R6 example lives with the
generator; the x
in Q7 lives with the instance. The R6
example goes on to show a solution with an separate initializer; the
same this not necessary in Q7, as the type definition itself is its
initializer(a separate initialize()
subroutine can be
defined to run once at an object’s initialization).
Simple <- type(function(){
x <- 1
getx <- function(){
x
}
}, "Simple")
Simple <- Simple %>% implement({
getx2 <- function(){
x * 2
}
})
Simple <- Simple %>% implement({
x <- 10
})
s <- Simple()
s$getx2()
#> [1] 20
In Q7, new code is simply appened to the old, meaning everything will be executed linearly from the beginning to the end. This make it inefficient when you replace something costly to make, like reading in a large amount of data or performing a lengthy calculation. In this case, it’s best to make a new type from scratch, or define a common prototype without the costly members.
Q7 type constructors need not (and cannot) be locked.
Simple <- type(function(){
x <- 1
getx <- function(){
x
}
}, "Simple")
s <- Simple()
s1 <- clone(s)
s1$x <- 2
s1$getx()
#> [1] 1
s$getx()
#> [1] 1
Deep Cloning
Simple <- type(function(){
x <- 1
}, "Simple")
Cloneable <- type(function(){
s <- NULL
s <- Simple()
}, "Cloneable")
c1 <- Cloneable()
c2 <- clone(c1)
c1$s$x <- 2
c2$s$x
#> [1] 1
The default clone()
behavior in Q7 is deep (recursive).
So any nested instances also gets cloned. Like in R6, only object
instances will be cloned deeply. The example of a custom
deep_clone
method in the R6 document is skipped for
brevity.
A <- type(function(){
private[finalize] <- function(.my){
base::print("Finalizer has been called!")
# Must always qualify `print()` with package name `base`,
# because it is masked by`print()` in the object masks
}
})
obj <- A()
rm(obj); gc()
#> [1] "Finalizer has been called!"
#> used (Mb) gc trigger (Mb) max used (Mb)
#> Ncells 644665 34.5 1103428 59 1103428 59.0
#> Vcells 1193840 9.2 8388608 64 2557661 19.6
For the finalizer function, you must define an argument
(.my
, but could be any name) to represent the object
itself.