18.6 Specialised data structures

There are two data structures and one special symbol that we need to cover for the sake of completeness. They are not usually important in practice.

18.6.1 Pairlists

Pairlists are a remnant of R’s past and have been replaced by lists almost everywhere. The only place you are likely to see pairlists in R66 is when working with calls to the function function, as the formal arguments to a function are stored in a pairlist:

f <- expr(function(x, y = 10) x + y)

args <- f[[2]]
args
#> $x
#> 
#> 
#> $y
#> [1] 10
typeof(args)
#> [1] "pairlist"

Fortunately, whenever you encounter a pairlist, you can treat it just like a regular list:

pl <- pairlist(x = 1, y = 2)
length(pl)
#> [1] 2
pl$x
#> [1] 1

Behind the scenes pairlists are implemented using a different data structure, a linked list instead of an array. That makes subsetting a pairlist much slower than subsetting a list, but this has little practical impact.

18.6.2 Missing arguments

The special symbol that needs a little extra discussion is the empty symbol, which is used to represent missing arguments (not missing values!). You only need to care about the missing symbol if you’re programmatically creating functions with missing arguments; we’ll come back to that in Section 19.4.3.

You can make an empty symbol with missing_arg() (or expr()):

missing_arg()
typeof(missing_arg())
#> [1] "symbol"

An empty symbol doesn’t print anything, so you can check if you have one with rlang::is_missing():

is_missing(missing_arg())
#> [1] TRUE

You’ll find them in the wild in function formals:

f <- expr(function(x, y = 10) x + y)
args <- f[[2]]
is_missing(args[[1]])
#> [1] TRUE

This is particularly important for ... which is always associated with an empty symbol:

f <- expr(function(...) list(...))
args <- f[[2]]
is_missing(args[[1]])
#> [1] TRUE

The empty symbol has a peculiar property: if you bind it to a variable, then access that variable, you will get an error:

m <- missing_arg()
m
#> Error in eval(expr, envir, enclos): argument "m" is missing, with no default

But you won’t if you store it inside another data structure!

ms <- list(missing_arg(), missing_arg())
ms[[1]]

If you need to preserve the missingness of a variable, rlang::maybe_missing() is often helpful. It allows you to refer to a potentially missing variable without triggering the error. See the documentation for use cases and more details.

18.6.3 Expression vectors

Finally, we need to briefly discuss the expression vector. Expression vectors are only produced by two base functions: expression() and parse():

exp1 <- parse(text = c("
x <- 4
x
"))
exp2 <- expression(x <- 4, x)

typeof(exp1)
#> [1] "expression"
typeof(exp2)
#> [1] "expression"

exp1
#> expression(x <- 4, x)
exp2
#> expression(x <- 4, x)

Like calls and pairlists, expression vectors behave like lists:

length(exp1)
#> [1] 2
exp1[[1]]
#> x <- 4

Conceptually, an expression vector is just a list of expressions. The only difference is that calling eval() on an expression evaluates each individual expression. I don’t believe this advantage merits introducing a new data structure, so instead of expression vectors I just use lists of expressions.


  1. If you’re working in C, you’ll encounter pairlists more often. For example, call objects are also implemented using pairlists.↩︎