Como extrair nomes de objetos da expressão
Dada uma expressão rlang:
expr1 <- rlang::expr({
d <- a + b
})
Como recuperar os nomes dos objetos referenciados na expressão?
> extractObjects(expr1)
[1] "d" "a" "b"
Melhor ainda, como recuperar os nomes dos objetos e categorizá-los por "obrigatório" (entrada) e "criado" (saída)?
> extractObjects(expr1)
$created [1] "d" $required
[1] "a" "b"
Respostas
A função de base all.varsfaz isso:
〉all.vars(expr1)
[1] "d" "a" "b"
Como alternativa, você pode usar all.namespara obter todos os nomes na expressão, em vez de apenas aqueles que não são usados como chamadas ou operadores:
〉all.names(expr1)
[1] "{" "<-" "d" "+" "a" "b"
Não se engane: este resultado está correto! Todos esses aparecem na expressão, não apenas a, be d.
Mas pode não ser o que você deseja.
Na verdade, estou assumindo que o que você deseja corresponde aos tokens de folha na árvore de sintaxe abstrata (AST) - em outras palavras, tudo, exceto chamadas de função (e operadores, que também são chamadas de função).
A árvore de sintaxe da sua expressão é a seguinte: 1
{
|
<-
/\
d +
/ \
a b
Obter essas informações significa caminhar pela AST:
leaf_nodes = function (expr) {
if(is.call(expr)) {
unlist(lapply(as.list(expr)[-1L], leaf_nodes))
} else {
as.character(expr)
}
}
〉leaf_nodes(expr1)
[1] "d" "a" "b"
Graças à representação AST, também podemos encontrar entradas e saídas:
is_assignment = function (expr) {
is.call(expr) && as.character(expr[[1L]]) %in% c('=', '<-', '<<-', 'assign')
}
vars_in_assign = function (expr) {
if (is.call(expr) && identical(expr[[1L]], quote(`{`))) {
vars_in_assign(expr[[2L]])
} else if (is_assignment(expr)) {
list(created = all.vars(expr[[2L]]), required = all.vars(expr[[3L]]))
} else {
stop('Expression is not an assignment')
}
}
〉vars_in_assign(expr1)
$created [1] "d" $required
[1] "a" "b"
Observe que esta função não lida com atribuições complexas (ou seja, coisas semelhantes d[x] <- a + bou f(d) <- a + bmuito bem.
1 lobstr::ast mostra a árvore de sintaxe de forma diferente, ou seja, como
█─`{` └─█─`<-` ├─d └─█─`+` ├─a └─b
… Mas a representação acima é mais convencional fora de R, e eu acho mais intuitiva.
Outra solução é extrair a árvore simbólica abstrata explicitamente :
getAST <- function(ee) purrr::map_if(as.list(ee), is.call, getAST)
str(getAST(expr1))
# List of 2
# $ : symbol { # $ :List of 3
# ..$ : symbol <- # ..$ : symbol d
# ..$ :List of 3 # .. ..$ : symbol +
# .. ..$ : symbol a # .. ..$ : symbol b
Em seguida, atravesse o AST para encontrar a (s) atribuição (ões):
extractObjects <- function(ast)
{
## Ensure that there is at least one node
if( length(ast) == 0 ) stop("Provide an AST")
## If we are working with the assigment
if( identical(ast[[1]], as.name("<-")) ) {
## Separate the LHS and RHS
list(created = as.character(ast[[2]]),
required = sapply(unlist(ast[[3]]), as.character))
} else {
## Otherwise recurse to find all assignments
rc <- purrr::map(ast[-1], extractObjects)
## If there was only one assignment, simplify reporting
if( length(rc) == 1 ) purrr::flatten(rc)
else rc
}
}
extractObjects( getAST(expr1) )
# $created # [1] "d" # # $required
# [1] "+" "a" "b"
Você pode então filtrar os operadores matemáticos , se necessário.
Isso é interessante. Acho que, conceitualmente, pode não estar claro em TODAS as expressões possíveis o que exatamente é entrada e saída. Se você olhar para a chamada árvore de sintaxe abstrata (AST), com a qual você pode visualizar lobstr::ast(), ela se parece com isto.
Portanto, em casos simples quando você sempre fez LHS <- operations on RHS variables, se você iterar no AST, sempre obterá o LST logo após o <-operador. Se você atribuir z <- rlang::expr(d <- a+b), então zse comporta como uma lista e você pode, por exemplo, fazer o seguinte:
z <- rlang::expr(d <- a+b)
for (i in 1:length(z)) {
if (is.symbol(z[[i]])) {
print(paste("Element", i, "of z:", z[[i]], "is of type", typeof(z[[i]])))
if (grepl("[[:alnum:]]", z[[i]])) {print(paste("Seems like", z[[i]], "is a variable"))}
} else {
for (j in 1:length(z[[i]])){
print(paste("Element", j, paste0("of z[[",i,"]]:"), z[[i]][[j]], "is of type", typeof(z[[i]][[j]])))
if (grepl("[[:alnum:]]", z[[i]][[j]])) {print(paste("Seems like", z[[i]][[j]], "is a variable"))}
}
}
}
#> [1] "Element 1 of z: <- is of type symbol"
#> [1] "Element 2 of z: d is of type symbol"
#> [1] "Seems like d is a variable"
#> [1] "Element 1 of z[[3]]: + is of type symbol"
#> [1] "Element 2 of z[[3]]: a is of type symbol"
#> [1] "Seems like a is a variable"
#> [1] "Element 3 of z[[3]]: b is of type symbol"
#> [1] "Seems like b is a variable"
Criado em 03-09-2020 pelo pacote reprex (v0.3.0)
Como você pode ver, essas árvores podem ficar rapidamente complicadas e aninhadas. Então, em um caso simples como no seu exemplo, assumindo que as variáveis estão usando representações alfanuméricas, podemos meio que identificar o que os "objetos" (como você os chama) são e quais são os operadores (que não correspondem ao [[:alnum:]]regex). Como você pode ver, o tipo não pode ser usado para distinguir entre objetos e operadores, pois é sempre symbol(btw zabaixo é um languageas z[[3]]está, por isso podemos condicionar se z[[i]]é symbolou não um e se não, cavar um nível mais profundo). Você poderia então (por sua conta e risco) tentar classificar que os objetos que aparecem imediatamente após a <-são "saídas" e o resto são "entradas", mas não tenho muita confiança nisso, especialmente para expressões mais complexas.
Em suma, tudo isso é muito especulativo.