Introduction to the Q7 Type System

Q7 type system provides an infrastructure to create objects in R; It is more advanced than native R classes, and is free from the grand narrative of hereditary OOP.

See other vignettes for :


Smart Objects
Compositional Construction
No Magic

Terms and Concepts

Q7 employs conventional OOP terms concepts, with some slight variations:

object - a unit of program and data, may refer to type or instance, or both

type - blueprint for an object

instance - an embodiment of a type

member - things bound to an object; some members are functions

method - a function that is bound and (usually) domestic to an object

Basic Interface

Make a type:

TypeOne <- type(function(arg1, arg2){
  var1 <- 3
  add <- function(){
    arg1 + arg2 + var1

Everything defined within the function’s closure become members of the object. The function’s arguments are accessible by bound functions of the object, but not become members themselves.

type_one <- TypeOne(1, 2)
#> [1] "add"  "var1"
# There's no `arg1` or `arg2` seen
#> [1] 6
# yet `add()` can see both arguments

Reserved Symbols

The following symbols are reserved by the Q7 type system and shall not be re-bound by the user.


Binding Modifiers:


Make Variants of an Object

There are two main strategies of extending an object: inheritance and composition. Q7 employs composition, and the benefit is obvious.

When you code with inheritance, your mind must navigate from sub- to super- classes from the inside out; Composition, on the other hand, is the linear addition to existing code, which is simpler for the mind to follow.

Types and instances can both be extended in the same manner. The concatenative nature of Q7 makes different objects truly independent from each other.

To extend an object, use implement(). If the object is a type, the resulting type must to be bound to a name; if the object is an instance, it is modified in place (see below). Modifying a type will not impact instances already created by the same type.

type_one %>% implement({
  substract <- function(){
    arg1 - arg2

Code can also be packaged with feature() for later use.

TypeTwo <- type(function(){
  n <- 10
hasFeatureOne <- feature({
  x <- 1
  x_plus_n <- function(){
    x + n
hasFeatureTwo <- feature({
  n <- 100 # Overwrites n from TypeTwo
  x <- 10 # Overwrites x from hasFeatureOne
  private[x_plus_n.old] <- x_plus_n 
    # Rename to preserve the old x_plus_n()
    # Mark private, because it is only going to be used by the new x_plus_n()
  x_plus_n <- function(){
    cat(sprintf("adding x (%i) to n (%i)...\n", x, n)) # do some extra thing
    x_plus_n.old() # call the old function
type_two_with_features <- TypeTwo() %>% 
  hasFeatureOne() %>% 

#> adding x (10) to n (100)...
#> [1] 110

Private Members

Any domestic function of an object can read from and write to the private environment. Remember to use the double arrow - <<- - because you want the assignment to pierce the function’s closure and reach the object itself.

Use caution: if the symbol you’re assigning to with <<- does not exist in either public or private environments of the object, it will end up somewhere outside the object, sometimes in the global environment.

Get Access to the Private Environment

As stated above, the private environment (.private) is parent of the public environment (.my). Parameters supplied to the arguments of the constructor function are implicitly private. When two members in private and public environments have the same name, they may co-exist. However, only the one in .my will win; the one in .private must be explicitly qualified.

The following code allows direct outside access to the count object.