Como usar o recurso de reticências de R ao escrever sua própria função?

186

A linguagem R possui um recurso bacana para definir funções que podem receber um número variável de argumentos. Por exemplo, a função data.frameaceita qualquer número de argumentos e cada argumento se torna os dados de uma coluna na tabela de dados resultante. Exemplo de uso:

> data.frame(letters=c("a", "b", "c"), numbers=c(1,2,3), notes=c("do", "re", "mi"))
  letters numbers notes
1       a       1    do
2       b       2    re
3       c       3    mi

A assinatura da função inclui reticências, assim:

function (..., row.names = NULL, check.rows = FALSE, check.names = TRUE, 
    stringsAsFactors = default.stringsAsFactors()) 
{
    [FUNCTION DEFINITION HERE]
}

Eu gostaria de escrever uma função que faça algo semelhante, pegando vários valores e consolidando-os em um único valor de retorno (além de fazer outro processamento). Para fazer isso, preciso descobrir como "descompactar" os ...argumentos da função dentro da função. Eu não sei como fazer isso. A linha relevante na definição de função de data.frameé object <- as.list(substitute(list(...)))[-1L], da qual não consigo entender.

Então, como posso converter as reticências da assinatura da função em, por exemplo, uma lista?

Para ser mais específico, como posso escrever get_list_from_ellipsisno código abaixo?

my_ellipsis_function(...) {
    input_list <- get_list_from_ellipsis(...)
    output_list <- lapply(X=input_list, FUN=do_something_interesting)
    return(output_list)
}

my_ellipsis_function(a=1:10,b=11:20,c=21:30)

Editar

Parece que existem duas maneiras possíveis de fazer isso. Eles são as.list(substitute(list(...)))[-1L]e list(...). No entanto, esses dois não fazem exatamente a mesma coisa. (Para as diferenças, veja exemplos nas respostas.) Alguém pode me dizer qual é a diferença prática entre eles e qual devo usar?

Ryan C. Thompson
fonte

Respostas:

113

Li respostas e comentários e vejo que poucas coisas não foram mencionadas:

  1. data.frameusa list(...)versão. Fragmento do código:

    object <- as.list(substitute(list(...)))[-1L]
    mrn <- is.null(row.names)
    x <- list(...)

    objecté usado para fazer mágica com nomes de colunas, mas xé usado para criar final data.frame.
    Para uso de ...argumento não avaliado, veja o write.csvcódigo onde match.callé usado.

  2. Conforme você escreve no resultado do comentário, a resposta Dirk não é uma lista de listas. É uma lista do comprimento 4, cujos elementos são do languagetipo. O primeiro objeto é um symbol- list, o segundo é a expressão 1:10e assim por diante. Isso explica por que [-1L]é necessário: remove os symbolargumentos esperados fornecidos em ...(porque é sempre uma lista).
    Como afirma Dirk substituteretorna "analisar a expressão não avaliada".
    Quando você chama my_ellipsis_function(a=1:10,b=11:20,c=21:30), ..."cria" uma lista de argumentos: list(a=1:10,b=11:20,c=21:30)e substitutefaça uma lista de quatro elementos:

    List of 4
    $  : symbol list
    $ a: language 1:10
    $ b: language 11:20
    $ c: language 21:30

    O primeiro elemento não tem um nome e isso está [[1]]na resposta Dirk. Consigo esses resultados usando:

    my_ellipsis_function <- function(...) {
      input_list <- as.list(substitute(list(...)))
      str(input_list)
      NULL
    }
    my_ellipsis_function(a=1:10,b=11:20,c=21:30)
  3. Como acima, podemos usar strpara verificar quais objetos estão em uma função.

    my_ellipsis_function <- function(...) {
        input_list <- list(...)
        output_list <- lapply(X=input_list, function(x) {str(x);summary(x)})
        return(output_list)
    }
    my_ellipsis_function(a=1:10,b=11:20,c=21:30)
     int [1:10] 1 2 3 4 5 6 7 8 9 10
     int [1:10] 11 12 13 14 15 16 17 18 19 20
     int [1:10] 21 22 23 24 25 26 27 28 29 30
    $a
       Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
       1.00    3.25    5.50    5.50    7.75   10.00 
    $b
       Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
       11.0    13.2    15.5    15.5    17.8    20.0 
    $c
       Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
       21.0    23.2    25.5    25.5    27.8    30.0 

    Está certo. Vamos ver a substituteversão:

       my_ellipsis_function <- function(...) {
           input_list <- as.list(substitute(list(...)))
           output_list <- lapply(X=input_list, function(x) {str(x);summary(x)})
           return(output_list)
       }
       my_ellipsis_function(a=1:10,b=11:20,c=21:30)
        symbol list
        language 1:10
        language 11:20
        language 21:30
       [[1]]
       Length  Class   Mode 
            1   name   name 
       $a
       Length  Class   Mode 
            3   call   call 
       $b
       Length  Class   Mode 
            3   call   call 
       $c
       Length  Class   Mode 
            3   call   call 

    Não é o que precisávamos. Você precisará de truques adicionais para lidar com esse tipo de objeto (como em write.csv).

Se você deseja usar ..., deve usá-lo como na resposta Shane, por list(...).

Marek
fonte
38

Você pode converter as reticências em uma lista com list()e, em seguida, executar suas operações nela:

> test.func <- function(...) { lapply(list(...), class) }
> test.func(a="b", b=1)
$a
[1] "character"

$b
[1] "numeric"

Portanto, sua get_list_from_ellipsisfunção nada mais é do que list.

Um caso de uso válido para isso é nos casos em que você deseja transmitir um número desconhecido de objetos para operação (como no seu exemplo de c()ou data.frame()). No ...entanto, não é uma boa ideia usá- lo quando você conhece cada parâmetro antecipadamente, pois adiciona alguma ambiguidade e complicação adicional à sequência de argumentos (e torna a assinatura da função pouco clara para qualquer outro usuário). A lista de argumentos é uma parte importante da documentação para usuários de funções.

Caso contrário, também é útil nos casos em que você deseja passar parâmetros para uma subfunção sem expor todos eles em seus próprios argumentos de função. Isso pode ser observado na documentação da função.

Shane
fonte
Eu sei sobre o uso das reticências como passagem para argumentos de subfunções, mas também é prática comum entre as primitivas R usar as reticências da maneira que descrevi. De fato, as funções liste cfuncionam dessa maneira, mas ambas são primitivas, portanto não posso inspecionar facilmente o código-fonte para entender como elas funcionam.
Ryan C. Thompson
rbind.data.frameuse desta maneira.
Marek
5
Se list(...)for suficiente, por que os Rins internos, como data.frameo formulário mais longo , usam as.list(substitute(list(...)))[-1L]?
Ryan C. Thompson
1
Como eu não criei data.frame, eu não sei a resposta para isso (o que disse, eu tenho certeza que não é uma boa razão para isso). Eu uso list()para esse fim em meus próprios pacotes e ainda tenho que encontrar um problema com ele.
Shane
34

Apenas para acrescentar às respostas de Shane e Dirk: é interessante comparar

get_list_from_ellipsis1 <- function(...)
{
  list(...)
}
get_list_from_ellipsis1(a = 1:10, b = 2:20) # returns a list of integer vectors

$a
 [1]  1  2  3  4  5  6  7  8  9 10

$b
 [1]  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20

com

get_list_from_ellipsis2 <- function(...)
{
  as.list(substitute(list(...)))[-1L]
}
get_list_from_ellipsis2(a = 1:10, b = 2:20) # returns a list of calls

$a
1:10

$b
2:20

Tal como está, qualquer uma das versões parece adequada para seus propósitos my_ellipsis_function, embora a primeira seja claramente mais simples.

Richie Cotton
fonte
15

Você já deu metade da resposta. Considerar

R> my_ellipsis_function <- function(...) {
+   input_list <- as.list(substitute(list(...)))
+ }
R> print(my_ellipsis_function(a=1:10, b=2:20))
[[1]]
list

$a
1:10

$b
11:20

R> 

Então, isso levou dois argumentos ae bda chamada e converteu-o em uma lista. Não foi isso que você pediu?

Dirk Eddelbuettel
fonte
2
Não é bem o que eu quero. Na verdade, isso parece retornar uma lista de listas. Observe o [[1]]. Além disso, gostaria de saber como funciona o encantamento mágico as.list(substitute(list(...))).
Ryan C. Thompson
2
O interno list(...)cria um listobjeto com base nos argumentos. Em seguida, substitute()cria a árvore de análise para a expressão não avaliada; consulte a ajuda para esta função. Bem como um bom texto avançado sobre R (ou S). Isso não é coisa trivial.
Dirk Eddelbuettel
Ok, e a [[-1L]]parte (da minha pergunta)? Não deveria ser [[1]]?
Ryan C. Thompson
3
Você precisa ler sobre a indexação. O sinal de menos significa 'excluir', ou seja print(c(1:3)[-1]), imprimirá apenas 2 e 3. A Lé uma maneira novos e desnecessários para garantir que ele acaba como um número inteiro, isso é feito muito em fontes de P.
Dirk Eddelbuettel
7
Eu não preciso de ler sobre a indexação, mas eu não preciso prestar mais atenção para a saída dos comandos que você mostra. A diferença entre o [[1]]e os $aíndices me fez pensar que listas aninhadas estavam envolvidos. Mas agora vejo que o que você realmente recebe é a lista que eu quero, mas com um elemento extra na frente. Então, isso [-1L]faz sentido. De onde vem esse primeiro elemento extra? E existe alguma razão para eu usar isso em vez de simplesmente list(...)?
Ryan C. Thompson
6

Isso funciona conforme o esperado. A seguir, uma sessão interativa:

> talk <- function(func, msg, ...){
+     func(msg, ...);
+ }
> talk(cat, c("this", "is", "a","message."), sep=":")
this:is:a:message.
> 

Mesmo, exceto com um argumento padrão:

> talk <- function(func, msg=c("Hello","World!"), ...){
+     func(msg, ...);
+ }
> talk(cat,sep=":")
Hello:World!
> talk(cat,sep=",", fill=1)
Hello,
World!
>

Como você pode ver, você pode usar isso para passar argumentos 'extras' para uma função dentro da sua função se os padrões não forem o que você deseja em um caso específico.

Overloaded_Operator
fonte