knitr::opts_chunk$set( collapse = TRUE, comment = "#>", error = TRUE )
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.
library(Q7)
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
ann <- Person("Ann", "black") ann
ann$hair ann$greet() ann$set_hair("red") ann$hair
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() q$remove()
q$queue q$length()
q$add(10)$add(11)$add(12)
q$remove() q$remove() q$remove() q$remove()
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 n$x2 n$x2 <- 1000 n$x n$rand n$rand n$rand <- 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() hq$remove() hq$show() hq$remove()
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() cq$add("z") cq$remove() cq$remove() cq$get_total()
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
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()
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() s$getx()
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
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.
prettyCountingQueue <- type(function(...){ extend(CountingQueue)(...) print <- function(){ cat("<PrettyCountingQueue> of ", get_total(), " elements\n", sep = "") } }, "prettyCountingQueue") pq <- prettyCountingQueue(1, 2, "foobar") pq
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()
For the finalizer function, you must define an argument (.my
, but could be any name) to represent the object itself.
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() a$fn()
b <- clone(a) b$get_my() b$fn()
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.