Entendendo o lambda do R
Uma das funcionalidades mais interessantes do R é a possibilidade de estender a linguagem para domínios específicos. A non-standard evaluation garante que até modificações à estrutura fundamental do R podem ser realizadas sem problema. Hoje vamos falar de um assunto que muita gente quer aprender, mas que pouca gente entende de verdade: funções anônimas.
Se você não souber o que é uma função anônima, pode ser que você conheça
esse conceito por outro nome. Também chamada de “função lambda”,
“notação de fórmula” ou “notação de til”, ela aparece principalmente em
programas que usam o {purrr} apesar de não serem exclusividade desse
pacote. Resumindo, se você já viu algo do tipo ~.x
e não entendeu do
que se tratava, este post é para você.
Introdução
Funções anônimas são, essencialmente, um jeito de simplificar a criação
de funções pequenas. Em poucas palavras, o nosso objetivo é não ter que
declarar uma função nova inteira com function() { ... }
para poder
usá-la dentro de um programa.
O exemplo que será utilizado na explicação a seguir será a função
conta_na()
que (não surpreendentemente) conta o número de NA
s em um
vetor. Vamos usá-la dentro de um map()
para que ela seja aplicada a
todas as colunas de um data frame. Sendo assim, partiremos da forma mais
verborrágica possível desse código e tentaremos chegar, intuitivamente,
nas funções anônimas.
Uma ressalva importante é que a notação explicada aqui só funciona
dentro do {tidyverse}! No final do texto será apresentada uma
alternativa que funciona fora desse contexto, mas, por enquanto, a
notação abaixo só pode aparecer nos argumentos .f
, .fn
e .fns
utilizados dentro do {tidyverse}.
Conceito
Vamos imaginar uma função conta_na()
que conta o número de NA
s em
uma coluna de um data frame. Para aplicá-la a todas as colunas do data
frame, podemos, por exemplo, utilizar a função map()
do pacote {purrr}
como no exemplo abaixo:
conta_na <- function(vetor) {
sum(is.na(vetor))
}
map_dbl(starwars, conta_na)
#> name height mass hair_color skin_color eye_color birth_year
#> 0 6 28 5 0 0 44
#> sex gender homeworld species films vehicles starships
#> 4 4 10 4 0 0 0
No R, quando temos uma função simples de uma linha, é perfeitamente possível não colocar o seu corpo em uma linha separada. Veja como o código já fica um pouco mais compacto (saída omitida daqui em diante):
conta_na <- function(vetor) { sum(is.na(vetor)) }
map_dbl(starwars, conta_na)
Agora, se temos uma função que cabe inteira em uma linha, o R nos
permite também omitir as chaves: por exemplo, m if-else pode ser escrito
if (cond) resp1 else resp2
se as respostas não tiverem mais de uma
linha. No nosso caso, vamos reduzir ainda mais a conta_na()
:
conta_na <- function(vetor) sum(is.na(vetor))
map_dbl(starwars, conta_na)
O próximo passo seria encurtar o nome do argumento da função, utilizando
algo mais genérico. Poucas pessoas sabem, mas o R permite nomes
começando com .
! Por motivos que ficarão claros a seguir, vamos
escolher .x
para ser o nome do nosso argumento:
conta_na <- function(.x) sum(is.na(.x))
map_dbl(starwars, conta_na)
Agora vem a grande sacada. Tudo que fizemos até agora funciona no R como um todo, mas, se atendermos algumas condições extras, podemos reduzir ainda mais essa função.
Vamos lá: se a função i) tiver apenas uma linha, ii) tiver apenas 1
argumento, iii) tiver .x
como seu único argumento .x
e iv) estiver
sendo utilizada como argumento de uma função do {tidyverse}, então
podemos omitir o function(.x)
(já que isso é informação redundante
dadas as condições) e trocá-lo por um singelo ~
:
conta_na <- ~ sum(is.na(.x))
map_dbl(starwars, conta_na)
O último passo é o motivo de chamarmos essa notação de “função anônima”.
Dado que nossa função já é tão pequena e utilizamos ela em apenas um
lugar, por que precisamos dar um nome para ela? É mais fácil declará-la
diretamente dentro do map()
sem um nome, ou seja, “anonimamente”:
map_dbl(starwars, ~ sum(is.na(.x)))
#> name height mass hair_color skin_color eye_color birth_year
#> 0 6 28 5 0 0 44
#> sex gender homeworld species films vehicles starships
#> 4 4 10 4 0 0 0
Pronto, agora você sabe o que significa uma função do tipo ~.x
. Para
treinar, tente fazer o processo reverso como no caso abaixo:
# Fração de valores distintos dentre todos
map_dbl(starwars, ~ length(unique(.x)) / length(.x))
# Desanonimizar
frac_distintos <- ~ length(unique(.x)) / length(.x)
map_dbl(starwars, frac_distintos)
# Remover a notação de til (não é mais necessário mexer no map())
frac_distintos <- function(.x) length(unique(.x)) / length(.x)
# Utilizar um nome melhor para o argumento
frac_distintos <- function(vec) length(unique(vec)) / length(vec)
# Recolocar as chaves
frac_distintos <- function(vec) { length(unique(vec)) / length(vec) }
# Identar o corpo da função
frac_distintos <- function(vec) {
length(unique(vec)) / length(vec)
}
Agora fica bem mais fácil de entender o que faz o map()
lá do começo.
Futuro
A pergunta óbvia agora é: existe um jeito de fazer algo assim fora do {tidyverse}? A resposta é sim e não.
Desde o R 4.1,
o R introduziu a sua própria notação anônima. Ela funciona de maneira
muito similar ao ~
, com a diferença de que você precisa dizer o nome
do seu argumento. Abaixo deixo vocês com o processo de simplificação da
função conta_na()
para a sua versão anônima que pode ser utilizada em
qualquer lugar e não só no {tidyverse}:
# Conta o número de NAs em um vetor
conta_na <- function(vetor) {
sum(is.na(vetor))
}
# Usar uma linha só
conta_na <- function(vetor) { sum(is.na(vetor)) }
# Sem necessidade de usar chaves
conta_na <- function(vetor) sum(is.na(vetor))
# Se a função tem uma linha, podemos usar a nova notação
conta_na <- \(vetor) sum(is.na(vetor))
# O nome do argumento pode ser qualquer coisa, não importa
conta_na <- \(v) sum(is.na(v))
# Anonimizar
map_dbl(starwars, \(v) sum(is.na(v)))
#> name height mass hair_color skin_color eye_color birth_year
#> 0 6 28 5 0 0 44
#> sex gender homeworld species films vehicles starships
#> 4 4 10 4 0 0 0
Quase tão bom quanto a notação de til!