Este é um tutorial sobre o pacote {rlang}
, um dos mais poderosos e menos
conhecidos do R. Ele é vital para a notação compacta do {tidyverse}
, conhecida
como tidy eval, mas mesmo assim poucas pessoas sabem como ele funciona e
como utilizá-lo para criar funções no estilo tidy.
O tutorial não é curto, mas fizemos o nosso melhor para começar com calma e terminar com colinhas para facilitar o uso deste material no dia a dia. Se for necessário, leia e releia uma mesma seção para ter certeza de que o conceito apresentado foi completamente absorvido. No caso de dúvidas, entre em contato conosco e com o resto da comunidade R no nosso Discourse.
Se estiver com preguiça, deixei uma colinha no final do post. Mas, para os
corajosos, preparem-se para alguns novos conceitos de programação, vários
filhotes de cachorro e muito {rlang}
!
O melhor amigo do R
A analogia que vamos usar para explicar o {rlang}
gira em torno de um parque
repleto de filhotes fofinhos. Nós temos uma única missão nesse parque: fazer
carinho nos cachorros. Para isso, temos uma função carinho()
que recebe o nome
de um filhote e imprime uma frase explicando em quem estamos fazendo carinho. O
objeto que descreve o filhote se resume a uma string com a sua cor.
rex <- "laranja"
carinho(rex)
#> Fazendo carinho no filhote laranja
Para facilitar a compreensão do código, vamos também ilustrar esse cenário. Na
figura abaixo, note que é criado um objeto rex
que recebe a figura de um
filhote laranja. A função carinho()
é retratada como uma mão parada que,
quando executada, retorna uma mão fazendo carinho no filhote de cor apropriada
(essencialmente a string retornada pelo código acima).
Expressões
Agora que temos uma base sólida para a analogia, podemos introduzir o primeiro
conceito importante do {rlang}
: expressões. Uma expressão no R não passa
do código antes antes que ele seja avaliado, ou seja, aquilo que você escreve e
que, depois de executado no console do RStudio, se torna um resultado. Em quase
100% dos casos, o R não faz nenhuma distinção entre a expressão e o valor que
ela retorna, de modo que executar carinho(rex)
fica equivalente a executar
carinho("laranja")
. Esse comportamento é chamado de avaliação ansiosa,
justamente porque o R avalia cada parte da expressão tão cedo quanto for
possível.
Essa, entretanto, não é única forma de avaliação. Também é possível capturar uma
expressão, “impedindo” o R de avaliá-la, em um processo denominado avaliação
preguiçosa. A função do {rlang}
que faz isso se chama expr()
e ela retorna
a expressão passada, vulgo o código escrito.
e <- expr(carinho(max))
e
#> carinho(max)
Veja que não importa que não existe ainda um filhote chamado max
! Como estamos
lidando apenas com uma expressão sem contexto, isso é perfeitamente possível.
Voltando para o nosso parque de cachorros, a avaliação preguiçosa se torna quase uma promessa de fazer carinho em um filhote. Temos toda a informação necessária (no caso, o nome do filhote), mas não transformamos isso na ação de fazer carinho: não “chamamos o filhote para perto” para acariciá-lo. Perceba que na figura abaixo não há as marcas de movimento da mão, pois estamos congelando a cena antes de o filhote vir até nós.
Ambientes
Na nossa analogia, o próximo conceito representa o parque em si, um lugar onde há uma correspondência entre nomes de cachorros. No R, um ambiente funciona como um dicionário que contém definições de objetos acompanhados pelos valores que eles carregam.
Abaixo, vamos “trazer” dois novos cachorros para o parque, ou seja, criar dois
novos objetos. A função env_print()
mostra todas as correspondências presentes
no ambiente (incluindo a da função carinho()
), além de algumas informações
extras que não nos interessam agora.
max <- "marrom"
dex <- "bege"
env_print()
#> <environment: global>
#> parent: <environment: package:rlang>
#> bindings:
#> * rex: <chr>
#> * e: <language>
#> * mtcars: <tibble[,11]>
#> * carinho: <fn>
#> * dex: <chr>
#> * max: <chr>
Na analogia, estamos colocando os cachorros max
e dex
no parque, permitindo
que possamos eventualmente fazer carinho neles. Vamos apenas ignorar a definição
da função carinho()
para que isso não atrapalhe o resto da explicação.
Perceba que o resultado da avaliação de uma expressão depende completamente do
ambiente. Na hora de executar um código, o R procura as definições de todos os
objetos no ambiente e os substitui dentro da expressão. Agora vamos ver o que
aconteceria se tentássemos fazer carinho no filhote chamado max
em outro
parque…
Avaliando expressões
Avaliação nua (bare evaluation no original) é o processo pelo qual o
{rlang}
permite que forneçamos explicitamente um ambiente no qual avaliar uma
expressão. É como se pudéssemos escolher o parque no qual vamos chamar um
filhote para acariciá-lo.
No código abaixo vamos visitar um outro parque, isto é, criar uma função. O
ambiente dentro de uma função herda as definições do ambiente global, mas
podemos fazer alterações lá dentro que não são propagadas para fora. Vide a
função abaixo: p()
define um objeto max
com a cor verde e avalia (ou seja,
executa) uma expressão lá dentro.
p <- function(x) {
max <- "verde"
eval_tidy(x)
}
Seguindo a analogia dos filhotes, é como se visitássemos um outro parque onde há
um cachorro chamado max
cuja cor é verde (além dos outros dois cachorros que
já havíamos visto no parque antigo).
Como a função eval_tidy()
, por padrão, utiliza o ambiente corrente para
avaliar expressões, então p(e)
deve indicar carinho em um filhote verde e
não mais em um filhote marrom. Note que, apesar de não estarmos passando um
ambiente explicitamente para a eval_tidy()
, ela está obtendo esse ambiente
através de caller_env()
, o valor padrão para seu argumento env
.
p(e)
#> Fazendo carinho no filhote verde
Na ilustração a seguir vemos o que aconteceria no nosso parque. Apesar de estarmos chamando o nome do cachorro marrom, como estamos em outro parque (uma nova função), o cachorro que responderá ao chamado aqui é verde!
Quosures
Agora que você entende o que é uma expressão, o que é um ambiente e como podemos
avaliar uma expressão dentro de um ambiente, chegou a hora de entender a
estrutura mais importante do {rlang}
: as quosures. Esse nome estranho vem
de quote e closure, dois conceitos extremamente importantes da Ciência da
Computação, mas explicar o que eles significam foge do escopo deste tutorial.
Uma quosure, apesar de parecer um conceito complexo, não passa de uma expressão
que carrega um apontador para seu ambiente consigo. Isso não parece ser muito
útil, mas é a quosure que permite que as funções do {tidyverse}
sejam capazes
de acessar as colunas de uma tabela e variáveis declaradas no ambiente global.
q <- quo(carinho(max))
q
#> <quosure>
#> expr: ^carinho(max)
#> env: global
Pensando nos filhotes, uma quosure é a promessa de fazer carinho em um cachorro
sabendo exatamente o endereço do parque em que ele estava Note que, na saída
acima, o env
se chama “global”, justamente porque estamos trabalhando
diretamente na sessão base do R.
Na figura abaixo, juntamente da cena retratada na ilustração sobre expressões,
vemos um qualificador de max
, especificando onde devemos encontrar ele. Isso
é significativamente diferente de simplesmente gritar pelo max
mais próximo.
Avaliando quosures
Assim como utilizamos a avaliação nua para obter o resultado de uma expressão em um certo ambiente, podemos usar a avaliação tidy (de tidy evaluation) para obter o resultado de uma quosure no ambiente que ela carrega.
Aqui, depois de capturar a quosure, podemos fazer o que quisermos com o ambiente
na qual avaliaremos ela, pois o único ambiente que importará na avaliação tidy
é o de seu ambiente de origem. Sendo assim, perceba que o argumento env
de
eval_tidy()
não foi levado em conta!
p(q)
#> Fazendo carinho no filhote marrom
É difícil traduzir esse processo para a analogia dos filhotes, mas seria algo
como voltar para o endereço do parque original antes de fazer carinho no filhote
cujo nome é max
.
Bang-bang
A peça final do quebra-cabeça do {rlang}
é o bang-bang, também conhecido
como quasiquotation e expresso na forma de duas exclamações: !!
. Essa
funcionalidade, exclusiva ao {rlang}
, permite que façamos uma “avaliação
ansiosa seletiva” em uma expressão ou quosure. Em breve ficará mais claro onde
isso pode ser útil, mas antes é necessário ver como usar o bang-bang na prática.
O bang-bang “força” a avaliação de uma parte da expressão, liberando o R para
fazer parte do seu trabalho de avaliação ansiosa. Veja, no código abaixo, como
funciona a captura de uma expressão que usa o bang-bang. Atente-se para o fato
de que, na seção anterior, alteramos o valor de max
.
expr(carinho(!!max))
#> carinho("marrom")
Na analogia dos filhotes, o bang-bang está essencialmente chamando um cachorro pelo nome antes que façamos carinho nele. Ao invés do contorno branco que vimos nas ilustrações sobre expressões e quosures, agora vemos a mão parada ao lado de um filhote específico.
De volta para casa
Apesar de termos visto um pouco de código R na sessão anterior, agora é
necessário aprofundar um pouco os exemplos. Prometo que não será nada muito
difícil, mas é impossível entender como aplicar o {rlang}
no mundo real sem
ver alguns casos de uso.
Na maior parte das ocasiões, não usaremos nenhuma das funções vistas até agora,
salvo pelo bang-bang (que na verdade não é uma função, mas sim uma sintaxe). O
principal uso do {rlang}
, na verdade, é capturar código que o usuário
escreve, então é necessário conhecer novas versões de expr()
e quo()
que são
capazes de capturar expressões e quosures vindas de fora de uma função.
Enriquecimento
O conceito de enriquecimento vem de uma analogia meio ruim criada pelo autor
do {rlang}
; para simplificar, pense que as versões enriquecidas de expr()
e
quo()
são mais “fortes” que as versões normais, sendo capazes de sair de
dentro de uma função para capturar expressões do lado de fora.
Abaixo é possível ver uma função que tenta capturar o nome de um filhote, mas é incapaz de fazê-lo por causa da avaliação ansiosa do R. O correto seria capturar a expressão escrita pelo usuário e imprimí-la como uma string.
nome1 <- function(filhote) {
cat("O nome do filhote é", filhote)
}
nome1(dex)
#> O nome do filhote é bege
Podemos ver a versão correta da função desejada em nome2()
. Ela captura a
expressão do usuário com enexpr()
(a versão enriquecida de expr()
) e
converte esse objeto em uma string com expr_text()
, permitindo que a função
imprima o nome do filhote.
nome2 <- function(filhote) {
nome <- enexpr(filhote)
cat("O nome do filhote é", expr_text(nome))
}
nome2(dex)
#> O nome do filhote é dex
Como não havia necessidade nenhuma de capturar o ambiente do usuário nesse
exemplo, usamos apenas enexpr()
. Na maioria das situações, entretanto, é
preciso usar enquo()
para obter o comportamento correto. Já que quosures
incluem expressões, expr()
e enexpr()
quase nunca são estritamente
necessárias, então vamos simplificar tudo e seguir apenas com as quosures.
No código abaixo a função explica()
precisa tanto da expressão quanto do
ambiente da mesma, ou seja, da quosure escrita pelo usuário.
explica <- function(acao) {
quosure <- enquo(acao)
cat("`", quo_text(quosure), "` retorna:\n", sep = "")
eval_tidy(quosure)
}
explica(carinho(dex))
#> `carinho(dex)` retorna:
#> Fazendo carinho no filhote bege
Preste bastante atenção em explica()
, pois pode ser que não seja fácil
entender como ela funciona. A primeira função utilizada é a enquo()
(quo()
enriquecida), que captura a expressão do usuário juntamente com o seu ambiente.
A seguir, temos apenas que converter a quosure em string com quo_text()
para
poder imprimí-la. O último passo é avaliar a quosure para obter um resultado
exatamente igual ao que o usuário obteria se decidisse executar a expressão
passada como argumento.
Curly-curly
A combinação da enquo()
com o bang-bang é justamente a forma correta de
implementar funções que trabalham com o {tidyverse}
. A função summarise()
,
por exemplo, não passa de um enquo()
disfarçado, o que quer dizer que podemos
usar o bang-bang para “injetar” o nome de uma variável dentro de um cálculo.
media1 <- function(df, var) {
summarise(df, resultado = mean(var))
}
media1(mtcars, cyl)
#> Error: Problem with `summarise()` column `resultado`.
#> ℹ `resultado = mean(var)`.
#> x object 'cyl' not found
O código acima, que não usa bang-bang, retorna um erro. O problema é que a
summarise()
está tentando tirar a média de um objeto chamado var
, que
carrega um objeto chamado cyl
, que simplesmente não existe no ambiente global.
Abaixo, usando bang-bang e enquo()
, o código funciona como esperado porque
mean(!!var)
se torna mean(cyl)
dentro da summarise()
.
media2 <- function(df, var) {
var <- enquo(var)
summarise(df, resultado = mean(!!var))
}
media2(mtcars, cyl)
#> # A tibble: 1 × 1
#> resultado
#> <dbl>
#> 1 6.19
O {tidyverse}
nos fornece um atalho para essa combinação poderosa de enquo()
com !!
: o {{ }}
, mais conhecido como curly-curly. Agora que você já
entende exatamente o que está acontecendo por trás dos panos, saber onde usar o
curly-curly é mais fácil.
media3 <- function(df, var) {
summarise(df, resultado = mean({{var}}))
}
media3(mtcars, cyl)
#> # A tibble: 1 × 1
#> resultado
#> <dbl>
#> 1 6.19
Splice
A penúltima funcionalidade do {rlang}
a compreender é o conceito de splice
(“emendar” em português), que se manifesta nas versões pluralizadas das funções
apresentadas até agora. Essencialmente, expr()/enexpr
e quo()/enquo()
só
conseguem lidar com uma única expressão ou quosure, então temos outras versões
para trabalhar com múltiplas expressões ou quosures.
Na prática, a principal função que utilizaremos é enquos()
. Ela captura todo o
conteúdo de uma elipse e o transforma em uma lista de quosures como no
exemplo abaixo. As versões plurais também acompanham o bang-bang-bang, o
irmão com splice do bang-bang.
media4 <- function(df, ...) {
vars <- enquos(...)
summarise(df, across(c(!!!vars), mean))
}
media4(mtcars, cyl, disp, hp)
#> # A tibble: 1 × 3
#> cyl disp hp
#> <dbl> <dbl> <dbl>
#> 1 6.19 231. 147.
Se você não estiver familiarizado com a across()
, basta saber apenas que o
primeiro argumento é um vetor de colunas (similar ao que passaríamos para
select()
) e o segundo é uma função para utilizar no resumo das colunas
especificadas. Aqui o !!!
está apenas transformando a chamada em
across(c(cyl, disp, hp), mean)
.
Símbolos
Existe ainda um conceito que não abordamos até agora: símbolos. Um símbolo
não passa do nome de um objeto, ou seja, rex
, max
e dex
na analogia dos
filhotes; mais especificamente, um símbolo é uma expressão com algumas
restrições sobre o seu conteúdo. A função sym()
, especificamente, transforma
uma string em um símbolo, permitindo que ela seja usada junto com outras
expressões.
media6 <- function(df, var) {
var <- ensym(var)
summarise(df, resultado = mean(!!var))
}
media6(mtcars, "cyl")
#> # A tibble: 1 × 1
#> resultado
#> <dbl>
#> 1 6.19
Miscelânea
Fora os vários conceitos já apresentados, restam apenas duas breves
considerações para praticamente zerar o {rlang}
:
O curly-curly funciona com strings, mas não com splice, então a família
sym()
torna-se quase desnecessária juntamente com o!!
ao mesmo tempo em que o!!!
permanece essencial.Há um operador específico (
:=
, chamado de morsa) para quando precisamos forçar a execução de algo no lado esquerdo de um cálculo, mesmo quando usando o curly-curly.
media7 <- function(df, col, ...) {
args <- enquos(...)
summarise(df, {{col}} := mean(!!!args))
}
media7(mtcars, "nova_coluna", drat, na.rm = TRUE)
#> # A tibble: 1 × 1
#> nova_coluna
#> <dbl>
#> 1 3.60
De volta para o trabalho
Depois de tanto conteúdo, agora você consegue entender as colinhas que
apresentamos abaixo para facilitar o seu uso do {rlang}
no dia-a-dia. Ao final
também deixamos as referências deste tutorial para que você possa aprofundar
ainda mais os seus conhecimentos de tidy eval.
Vocabulário
Vocábulo | Tradução | Significado | Código |
---|---|---|---|
Ambiente | Environment | Dicionário de nomes e valores |
env()
|
Avaliação ansiosa | Eager evaluation | Avaliação de todo objeto o mais rápido possível |
|
Avaliação nua | Bare evaluation | Avaliação que necessita de um ambiente passado explicitamente |
eval_tidy(e)
|
Avaliação preguiçosa | Lazy evaluation | Avaliação de cada objeto conforme a necessidade |
quo() , etc.
|
Avaliação tidy | Tidy evaluation | Avaliação que utiliza o ambiente da quosure |
eval_tidy(q)
|
Bang-bang | Bang-bang | Operador utilizado para forçar a avaliação de um objeto |
!!
|
Bang-bang-bang | Bang-bang-bang | Operador utilizado para forçar a avaliação de vários objetos |
!!!
|
Curly-curly | Curly-curly |
Atalho para enquo() + !!
|
{{ }}
|
Elipse | Ellipsis | Argumento de uma funcão que pode receber múltiplas entradas |
...
|
Enriquecimento | Enriching | Processo que permite a captura de código do usuário |
enquo() , etc.
|
Expressão | Expression | Código R antes de avaliado |
expr()
|
Morsa | Walrus | Operador utilizado para permitir expressões do lado esquerdo de uma igualdade |
:=
|
Quosure | Quosure | Expressão que carrega seu ambiente consigo |
quo()
|
Símbolo | Symbol | Expressão que pode representar apenas o nome de um objeto |
sym()
|
Splice | Splice | Processo que permite a captura de múltiplas expressões, etc. |
quos() , etc.
|
Principais funções
Objeto | Simples | Enriquecido |
---|---|---|
Símbolo |
sym()/syms()
|
ensym()/ensyms()
|
Expressão |
expr()/exprs()
|
enexpr()/enexprs()
|
Quosure |
quo()/quos()
|
enquo()/enquos()
|
Principais padrões
Os padrões incluem dois exemplos que não foram explicados durante o tutorial. Para saber mais, consulte as referências no final do texto.
Descrição | Usuário | Programador |
---|---|---|
Expressão do lado esquerdo |
media(df, col)
|
{{col}} := mean(var)
|
Expressão do lado direito |
media(df, var)
|
col = mean({{var}})
|
Expressões do lado direito |
media(df, var, arg1 = 0)
|
col = mean(!!!var)
|
Expressões no meio |
agrupar(df, var1, var2)
|
group_by(df, ...)
|
Símbolo do lado esquerdo |
media(df, "col")
|
{{col}} := mean(var)
|
Símbolo do lado direito |
media(df, "var")
|
col = mean(.data$var)
|