¿Cómo usar la función de puntos suspensivos de R cuando escribes tu propia función?

186

El lenguaje R tiene una característica ingeniosa para definir funciones que pueden tomar un número variable de argumentos. Por ejemplo, la función data.frametoma cualquier número de argumentos, y cada argumento se convierte en los datos para una columna en la tabla de datos resultante. Ejemplo 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

La firma de la función incluye puntos suspensivos, como este:

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

Me gustaría escribir una función que haga algo similar, tomar múltiples valores y consolidarlos en un solo valor de retorno (además de realizar algún otro procesamiento). Para hacer esto, necesito descubrir cómo "desempaquetar" los ...argumentos de la función dentro de la función. No se como hacer esto. La línea relevante en la definición de la función de data.framees object <- as.list(substitute(list(...)))[-1L], que no puedo entender.

Entonces, ¿cómo puedo convertir los puntos suspensivos de la firma de la función en, por ejemplo, una lista?

Para ser más específico, ¿cómo puedo escribir get_list_from_ellipsisen el código a continuación?

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 hay dos formas posibles de hacer esto. Son as.list(substitute(list(...)))[-1L]y list(...). Sin embargo, estos dos no hacen exactamente lo mismo. (Para ver las diferencias, vea ejemplos en las respuestas). ¿Alguien puede decirme cuál es la diferencia práctica entre ellos y cuál debo usar?

Ryan C. Thompson
fuente

Respuestas:

113

Leo respuestas y comentarios y veo que pocas cosas no fueron mencionadas:

  1. data.frameutiliza la list(...)versión Fragmento del código:

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

    objectse usa para hacer algo de magia con los nombres de columna, pero xse usa para crear el final data.frame.
    Para el uso de ...argumentos no evaluados, mire el write.csvcódigo donde match.callse usa.

  2. Mientras escribe en el resultado del comentario, la respuesta Dirk no es una lista de listas. Es una lista de longitud 4, cuyos elementos son de languagetipo. El primer objeto es un symbol- list, el segundo es expresión 1:10y así sucesivamente. Eso explica por qué [-1L]es necesario: elimina los esperados symbolde los argumentos proporcionados en ...(porque siempre es una lista).
    Como dice Dirk, substitutedevuelve "árbol de análisis de la expresión no evaluada".
    Cuando llame my_ellipsis_function(a=1:10,b=11:20,c=21:30), ..."crea" una lista de argumentos: list(a=1:10,b=11:20,c=21:30)y substituteconviértala en una lista de cuatro elementos:

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

    El primer elemento no tiene nombre y esto está [[1]]en respuesta Dirk. Logro estos 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 arriba, podemos usar strpara verificar qué objetos están en una función.

    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á bien. Veamos la substituteversión:

       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 
    

    No es lo que necesitábamos. Necesitará trucos adicionales para lidiar con este tipo de objetos (como en write.csv).

Si desea usarlo ..., debe usarlo como en la respuesta de Shane, por list(...).

Marek
fuente
38

Puede convertir los puntos suspensivos en una lista con list(), y luego realizar sus operaciones en ella:

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

$b
[1] "numeric"

Entonces tu get_list_from_ellipsisfunción no es más que list.

Un caso de uso válido para esto es en los casos en que desea pasar un número desconocido de objetos para la operación (como en su ejemplo de c()o data.frame()). Sin ...embargo, no es una buena idea usar el cuando conoce cada parámetro de antemano, ya que agrega cierta ambigüedad y más complicación a la cadena de argumento (y hace que la firma de la función no sea clara para ningún otro usuario). La lista de argumentos es una pieza importante de documentación para los usuarios de funciones.

De lo contrario, también es útil para casos en los que desea pasar parámetros a una subfunción sin exponerlos a todos en sus propios argumentos de función. Esto se puede observar en la documentación de la función.

Shane
fuente
Sé sobre el uso de puntos suspensivos como paso a través de argumentos para subfunciones, pero también es una práctica común entre los primitivos R usar los puntos suspensivos de la manera que he descrito. De hecho, las funciones listy cfuncionan de esta manera, pero ambas son primitivas, por lo que no puedo inspeccionar fácilmente su código fuente para comprender cómo funcionan.
Ryan C. Thompson
rbind.data.frameusa de esta manera.
Marek
55
Si list(...)es suficiente, ¿por qué las R incorporadas, como data.frameusar la forma más larga en su as.list(substitute(list(...)))[-1L]lugar?
Ryan C. Thompson
1
Como yo no he creado data.frame, no sé la respuesta a eso (que dijo, estoy seguro de que no es una buena razón para ello). Lo uso list()para este propósito en mis propios paquetes y todavía no he encontrado un problema con él.
Shane
34

Solo para agregar a las respuestas de Shane y Dirk: es interesante 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

con

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á, cualquiera de las versiones parece adecuada para sus propósitos my_ellipsis_function, aunque la primera es claramente más simple.

Algodón Richie
fuente
15

Ya diste la mitad de la respuesta. 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> 

Entonces esto tomó dos argumentos ay bde la llamada y lo convirtió en una lista. ¿No fue eso lo que pediste?

Dirk Eddelbuettel
fuente
2
No es exactamente lo que quiero. Eso realmente parece devolver una lista de listas. Note el [[1]]. Además, me gustaría saber cómo funciona el encantamiento mágico as.list(substitute(list(...))).
Ryan C. Thompson
2
Lo interno list(...)crea un listobjeto basado en los argumentos. Luego substitute()crea el árbol de análisis para la expresión no evaluada; Vea la ayuda para esta función. Además de un buen texto avanzado sobre R (o S). Esto no es algo trivial.
Dirk Eddelbuettel
Ok, ¿qué pasa con la [[-1L]]parte (de mi pregunta)? ¿No debería ser [[1]]?
Ryan C. Thompson
3
Necesita leer sobre indexación. El signo menos significa 'excluir', es decir print(c(1:3)[-1]), imprimirá solo 2 y 3. Esta Les una nueva forma de asegurarse de que termine como un entero, esto se hace mucho en las fuentes R.
Dirk Eddelbuettel
77
No necesito leer sobre indexación, pero tengo que prestar más atención a la salida de los comandos que muestra. La diferencia entre el [[1]]y los $aíndices me hizo pensar que las listas anidadas estaban involucradas. Pero ahora veo que lo que realmente obtienes es la lista que quiero, pero con un elemento adicional al frente. Entonces, [-1L]tiene sentido. ¿De dónde viene ese primer elemento extra? ¿Y hay alguna razón por la que debería usar esto en lugar de simplemente list(...)?
Ryan C. Thompson
6

Esto funciona como se esperaba. La siguiente es una sesión interactiva:

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

Lo mismo, excepto con un argumento predeterminado:

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

Como puede ver, puede usar esto para pasar argumentos 'adicionales' a una función dentro de su función si los valores predeterminados no son lo que desea en un caso particular.

Overloaded_Operator
fuente