Três Pontinhos: Como Funcionam os Dots

Chamdos de “três pontinhos”, “reticências”, “dots” ou “ellipsis”, os ... são uma das funcionalidades mais comuns do R, mas ao mesmo tempo uma das menos conhecidas. Explicá-los em linguagem técnica é muito simples: eles são os argumentos variádicos do R! O difícil é entender de verdade o que eles são e como usá-los. Vamos abandonar o jargão e sigamos em frente, agora em bom português…

Obs.: O nome correto no R para os ... é dots, então vou usar esse termo a partir de agora. A prova disso é que, para consultar a sua documentação, executamos ?dots.

Onde estão

Como eu disse anteriormente, eles são bastante comuns, mas quão comuns exatamente? Talvez mais do que você imagine. Veja abaixo os protótipos de algumas poucas funções que talvez você conheça (ignore o NULL, ele é parte da saída da função args()):

args(sum)
## function (..., na.rm = FALSE) 
## NULL
args(c)
## function (...) 
## NULL
args(dplyr::mutate)
## function (.data, ...) 
## NULL

Te convenci? Entender os dots é, portanto, uma excelente arma no arsenal do programador de R, tanto que eles são usados pelas funções mais importantes da linguagem toda.

O que são

De forma bem geral, os dots são um argumento que, quando colocado na sua função, pode ser substituído por qualquer coisa pelo usuário. Na função sum(), por exemplo, os dots podem virar uma série de números (quantos o usuário quiser).

sum(1, 2, 3, 4, 5)
## [1] 15

Quando falamos de argumentos normais, não precisamos declarar seus argumentos caso estejamos utilizando-os na ordem correta. Os dots, entretanto, podem ser substituídos por qualquer número de objetos, então eles quebram essa regra; qualquer argumento que vier depois dos dots precisa ser nomeado.

# Não funciona do jeito esperado (TRUE mais um elemento dos dots)
sum(1, 2, NA, 4, 5, TRUE)
## [1] NA
# Agora sim
sum(1, 2, NA, 4, 5, na.rm = TRUE)
## [1] 12

Sem os dots a função sum() estaria limitada a receber um vetor de números, mas com essa ferramenta ela passa a poder receber números separados como se fossem cada um um argumento. A função c(), entretanto, não poderia ser implementada sem os dots.

O seu poder completo, porém, fica mais claro na função dplyr::select(). Aqui vemos que podemos até dar nomes arbitrários para os “argumentos” de dentro dos dots e a função pode usá-los sem o menor problema:

mtcars |>
  dplyr::select(mpg, cil = cyl, marcha = gear) |>
  head()
##                    mpg cil marcha
## Mazda RX4         21.0   6      4
## Mazda RX4 Wag     21.0   6      4
## Datsun 710        22.8   4      4
## Hornet 4 Drive    21.4   6      3
## Hornet Sportabout 18.7   8      3
## Valiant           18.1   6      3

Perceba que a função dplyr::select() não tem como saber quantas colunas nós vamos selecionar e quais nomes eu vou dar para cada uma, tornando impossível o uso de argumentos convencionais. O uso do dots é inevitável nesses casos.

Como usá-los

Agora que já vimos universalidade e importância dos dots, chegou a hora de entender como eles funcionam e como usá-los. Vamos começar com um exemplo simples: criar uma função que captura quaisquer argumentos que o usuário resolver passar e imprime seus valores.

# Captura dots e os imprime como uma lista
captura <- function(...) {
  list(...)
}

captura(arg1 = 1, arg2 = "b", arg3 = FALSE)
## $arg1
## [1] 1
## 
## $arg2
## [1] "b"
## 
## $arg3
## [1] FALSE

Simples, né? 90% das vezes podemos simplesmente transformar os dots em uma lista comum com list(...) e utilizá-la normalmente. Em breve ficará mais claro por que isso funciona.

Se quisermos capturar argumentos específicos dentro dos dots, aí podemos usar uma função especial chamada ...elt() (sim, as reticências fazem parte de seu nome):

# Captura dots e os imprime como uma lista
captura_segundo <- function(...) {
  ...elt(2)
}

captura_segundo(arg1 = 1, arg2 = "b", arg3 = FALSE)
## [1] "b"

A terceira forma de usar os dots é os transportando para uma função que recebe dots. Como já deve ter ficado evidente, os dots podem ser substituídos por qualquer número de argumentos por parte do usuário, mas eles também podem ser passados como argumento no lugar dos dots de outra função!

filtra_seleciona <- function(marchas, ...) {
  mtcars |>
    dplyr::filter(gear == marchas) |>
    dplyr::select(...) |>
    head()
}

filtra_seleciona(4, mpg, cil = cyl)
##                mpg cil
## Mazda RX4     21.0   6
## Mazda RX4 Wag 21.0   6
## Datsun 710    22.8   4
## Merc 240D     24.4   4
## Merc 230      22.8   4
## Merc 280      19.2   6

No caso acima, os dots eram mpg, cil = cyl e isso foi transportado perfeitamente para dentro de dplyr::select().

Para fechar este tutorial com chave de ouro, vamos criar uma função arbitrária que precisa de um argumento depois dos dots: nossa função deve receber qualquer quantidade de valores numéricos, ignorar o primeiro e somar o resto com n.

ignora_um_soma_n <- function(..., n = 0) {
  valores <- list(...)
  valores <- valores[-1]
  unlist(valores) + n
}

ignora_um_soma_n(1, 2, 3, 4, 5, n = 10)
## [1] 12 13 14 15

Espero que agora esteja pelo menos um pouco mais claro o funcionamento dos dots! Se não, pode entrar em contato comigo via Twitter ou postar uma dúvida no nosso fórum.

comments powered by Disqus