Comparison to R6

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.

Terminologies

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.

library(Q7)

Basics

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>
ann <- Person("Ann", "black")
ann
#> <Q7instance:Person>
#> - greet: <function>
#> - hair: <character>
#> - name: <character>
#> - set_hair: <function>
ann$hair
#> [1] "black"
ann$greet()
#> Hello, my name is Ann.
ann$set_hair("red")
ann$hair
#> [1] "red"

Private members

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")
q$add("something")
q$add("another thing")
q$add(17)
q$remove()
#> [1] 5
q$remove()
#> [1] 6
q$queue
#> NULL
q$length()
#> Error: attempt to apply non-function
q$add(10)$add(11)$add(12)
q$remove()
#> [1] "foo"
q$remove()
#> [1] "something"
q$remove()
#> [1] "another thing"
q$remove()
#> [1] 17

Active Bindings

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

Fields containing reference objects

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

Other topics

Adding members to an existing class

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.

Cloning Objects

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.

Printing Q7 objects to the screen

prettyCountingQueue <- type(function(...){
  extend(CountingQueue)(...)
  print <- function(){
    cat("<PrettyCountingQueue> of ", get_total(), " elements\n", sep = "")
  }
}, "prettyCountingQueue")

pq <- prettyCountingQueue(1, 2, "foobar")
pq
#> <PrettyCountingQueue> of 3 elements

Finalizers

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.

Class methods vs. member functions

In Q7 context, domestic functions vs foreign functions

FunctionWrapper <- type({
  fn <- NULL
  get_my <- function(){
    .my
  }
})

a <- FunctionWrapper()

.my <- 100
a$fn <- function(){
  .my
}

a$get_my()
#> Error: attempt to apply non-function

a$fn()
#> [1] 100
b <- clone(a)

b$get_my()
#> Error: attempt to apply non-function
b$fn()
#> [1] 100