A love story among strings, symbols and calls

2019/09/05

Non-standard evaluation (NSE) is a cornerstone of tidyverse packages like dpylr and ggplot2. It enables R to generate code systematically and evaluates it in a specific environment.

Generating code is based on expressions. As what documented in AdvanceR Chap18.1, expressions are implemented to

separate our description of the action from the action itself.

Expressions are created using rlang::expr() and they mainly include

This post explores what are them and how to convert among them using functions in rlang packagehttps://cran.r-project.org/web/packages/rlang/index.html, aiming to nail down those jaggons in a more intuitive way. To this end, I characterized functions based on input/output, although they might be some nitty-gritty details for choosing one function over the other in a specific context.

I defined two sets of testing variables, each corresponds to a type of strings: the first set is single object that could be used as a name of an object; the second set contains operators that could be parse as a function call. Each set consists of expression of a string, string itself and expression of unquoted.

so <- expr("x") # single string expression 
ss <- "x" # single string
se <- expr(x) # single unquoted expression

fo <- expr("x + 1") # function string expression 
fs <- "x + 1" # function string
fe <- expr(x + 1) # function expression

Here I will show the following conversion rules using examples. A flow chart summarises the conversion rules.

Constant strings are self quoting.

so == ss # expr("x") == "x"
## [1] TRUE
fo == fs # expr("x + 1") == "x + 1"
## [1] TRUE

Captured expression depends on input.

str(se) # se <- expr(x)
##  symbol x
str(fe) # fe <- expr(x + 1)
##  language x + 1

Capturing the name of captured expression is useless.

str(expr(se)) # symbol se
##  symbol se
str(expr(fe)) # symbol fe 
##  symbol fe

Both unquoting and enriched expression could recover quote.

expr(!!ss) == enexpr(ss) # == "x"; chr "x"
## [1] TRUE
expr(!!se) == enexpr(se) # == expr(x); symbol x
## [1] TRUE
expr(!!fs) == enexpr(fs) # == "x + 1"; chr "x + 1"
## [1] TRUE
expr(!!fe) == enexpr(fe) # == expr(x + 1); language x + 1
## [1] TRUE

Parsing a string, not a call, to a call.

str(parse_expr(ss)) # symbol x
##  symbol x
parse_expr(ss) == se # TRUE
## [1] TRUE
# parse_expr(se) # error: must be a character vector, not symbol

str(parse_expr(fs)) # language x + 1
##  language x + 1
parse_expr(fs) == fe # TRUE
## [1] TRUE
# parse_expr(fe) # error: must be a character vector, not function call

call2() is another way to construct function from symbols

call2("+", expr(x), 1) == fe 
## [1] TRUE

ensym()/sym() converts string to symbol but not to call.

str(ensym(ss)) # == sym("x")
##  symbol x
str(ensym(se)) # == sym(expr(x))
##  symbol x
str(ensym(fs)) # == sym("x + 1")
##  symbol x + 1
# str(ensym(fe)) # == sym(expr(x + 1)) # error

parse_expr() and deparse() are not perfectly symmetric, but expr_text might be better?

(symbol <- ensym(fs))
## `x + 1`
str(symbol)
##  symbol x + 1
(call <- parse_expr(deparse(symbol)))
## x + 1
str(call)
##  language x + 1
(call <- parse_expr(expr_text(symbol)))
## `x + 1`
str(call)
##  symbol x + 1