Como extrair nomes de objetos da expressão

Sep 04 2020

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

2 KonradRudolph Sep 04 2020 at 02:05

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.

1 ArtemSokolov Sep 04 2020 at 02:01

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.

ValeriVoev Sep 04 2020 at 01:44

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.