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
- constant:
rlang::is_syntactic_literal()
is TRUE. Only character vector will be discuss here since both “description of the action” and “action itself” are generally generated from string. Therefore,str
returnschr
. - symbol:
str
returns "symbol andrlang::is_symbol()
is TRUE and - call:
str
returns “language” andrlang::is_call()
is TRUE. (AdvanceR Chap18.3.4 summary)
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