Comment utiliser la fonction de sélection de R lors de l'écriture de votre propre fonction?

190

Le langage R a une fonctionnalité astucieuse pour définir des fonctions qui peuvent prendre un nombre variable d'arguments. Par exemple, la fonction data.frameprend n'importe quel nombre d'arguments et chaque argument devient les données d'une colonne dans la table de données résultante. Exemple d'utilisation:

> 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 signature de la fonction comprend des points de suspension, comme ceci:

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

Je voudrais écrire une fonction qui fait quelque chose de similaire, en prenant plusieurs valeurs et en les consolidant en une seule valeur de retour (ainsi qu'en effectuant un autre traitement). Pour ce faire, j'ai besoin de comprendre comment "décompresser" les ...arguments de la fonction dans la fonction. Je ne sais pas comment faire ça. La ligne pertinente dans la définition de fonction dedata.frame est object <- as.list(substitute(list(...)))[-1L], dont je ne peux pas comprendre.

Alors, comment puis-je convertir les points de suspension de la signature de la fonction en, par exemple, une liste?

Pour être plus précis, comment puis-je écrire get_list_from_ellipsisdans le code ci-dessous?

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)

Éditer

Il semble qu'il existe deux façons possibles de procéder. Ils sont as.list(substitute(list(...)))[-1L]et list(...). Cependant, ces deux ne font pas exactement la même chose. (Pour les différences, voir des exemples dans les réponses.) Quelqu'un peut-il me dire quelle est la différence pratique entre eux et laquelle je devrais utiliser?

Ryan C. Thompson
la source

Réponses:

115

J'ai lu les réponses et les commentaires et je vois que peu de choses n'ont pas été mentionnées:

  1. data.frameutilise la list(...)version. Fragment du code:

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

    objectest utilisé pour faire de la magie avec les noms de colonne, mais xest utilisé pour créer final data.frame.
    Pour utiliser un ...argument non évalué , regardez le write.csvcode où il match.callest utilisé.

  2. Au fur et à mesure que vous écrivez dans le résultat du commentaire dans Dirk, la réponse n'est pas une liste de listes. Est une liste de longueur 4, dont les éléments sont de languagetype. Le premier objet est un symbol- list, le second est l'expression 1:10et ainsi de suite. Cela explique pourquoi [-1L]est nécessaire: il supprime les symbolarguments attendus des arguments fournis dans ...(car il s'agit toujours d'une liste).
    Comme Dirk déclare, substituterenvoie "parse tree l'expression non évaluée".
    Lorsque vous appelez my_ellipsis_function(a=1:10,b=11:20,c=21:30)alors ..."crée" une liste d'arguments: list(a=1:10,b=11:20,c=21:30)et substitutefaites-en une liste de quatre éléments:

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

    Le premier élément n'a pas de nom et c'est [[1]]dans la réponse de Dirk. J'obtiens ces résultats en utilisant:

    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. Comme ci-dessus, nous pouvons utiliser strpour vérifier quels objets se trouvent dans une fonction.

    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 
    

    C'est bon. Permet de voir la substituteversion:

       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'est-ce pas ce dont nous avions besoin. Vous aurez besoin d'astuces supplémentaires pour gérer ce type d'objets (comme dans write.csv).

Si vous voulez l'utiliser, ...vous devez l'utiliser comme dans la réponse de Shane, par list(...).

Marek
la source
38

Vous pouvez convertir les points de suspension en une liste avec list(), puis effectuer vos opérations dessus:

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

$b
[1] "numeric"

Donc, votre get_list_from_ellipsisfonction n'est rien de plus list.

Un cas d'utilisation valide pour cela est dans les cas où vous souhaitez passer un nombre inconnu d'objets pour l'opération (comme dans votre exemple de c()ou data.frame()). Ce n'est pas une bonne idée d'utiliser le...Cependant, ce lorsque vous connaissez chaque paramètre à l'avance, car cela ajoute une ambiguïté et une complication supplémentaire à la chaîne d'argument (et rend la signature de la fonction peu claire pour tout autre utilisateur). La liste d'arguments est un élément important de la documentation pour les utilisateurs de fonctions.

Sinon, il est également utile dans les cas où vous souhaitez passer des paramètres à une sous-fonction sans les exposer tous dans vos propres arguments de fonction. Cela peut être noté dans la documentation de la fonction.

Shane
la source
Je sais comment utiliser les points de suspension comme un passage pour les arguments aux sous-fonctions, mais il est également courant chez les primitives R d'utiliser les points de suspension de la manière que j'ai décrite. En fait, les fonctions listet cfonctionnent de cette manière, mais les deux sont des primitives, donc je ne peux pas facilement inspecter leur code source pour comprendre comment elles fonctionnent.
Ryan C. Thompson
rbind.data.frameutiliser de cette façon.
Marek
5
Si cela list(...)est suffisant, pourquoi les fonctions intégrées R telles que l' data.frameutilisation de la forme plus longue à la as.list(substitute(list(...)))[-1L]place?
Ryan C. Thompson
1
Comme je l' ai pas créé data.frame, je ne connais pas la réponse à cette question (qui a dit, je suis sûr qu'il y a une bonne raison pour cela). J'utilise list()à cette fin dans mes propres packages et je n'ai pas encore rencontré de problème avec celui-ci.
Shane
34

Juste pour ajouter aux réponses de Shane et Dirk: il est intéressant de comparer

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

avec

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

Dans l'état actuel des choses, les deux versions semblent adaptées à vos besoins my_ellipsis_function, bien que la première soit clairement plus simple.

Coton Richie
la source
16

Vous avez déjà donné la moitié de la réponse. Considérer

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> 

Donc, cela a pris deux arguments aet bde l'appel et l'a converti en une liste. N'est-ce pas ce que vous avez demandé?

Dirk Eddelbuettel
la source
2
Pas tout à fait ce que je veux. Cela semble en fait renvoyer une liste de listes. Remarquez le [[1]]. Aussi, j'aimerais savoir comment fonctionne l'incantation magique as.list(substitute(list(...))).
Ryan C. Thompson
2
L'intérieur list(...)crée un listobjet basé sur les arguments. substitute()Crée ensuite l'arborescence d'analyse pour l'expression non évaluée; voir l'aide de cette fonction. Ainsi qu'un bon texte avancé sur R (ou S). Ce ne sont pas des choses triviales.
Dirk Eddelbuettel
Ok, qu'en est-il de la [[-1L]]partie (d'après ma question)? Ne devrait-il pas l'être [[1]]?
Ryan C. Thompson
3
Vous devez vous renseigner sur l'indexation. Le moins signifie «exclure», c'est-à-dire print(c(1:3)[-1])n'imprimera que 2 et 3. Le Lest un moyen novateur de s'assurer qu'il finit par être un entier, cela se fait souvent dans les sources R.
Dirk Eddelbuettel
7
Je ne ai pas besoin de lire sur l' indexation, mais je ne dois accorder plus d' attention à la sortie des commandes vous montrer. La différence entre les [[1]]et les $aindices m'a fait penser que les listes imbriquées étaient impliquées. Mais maintenant, je vois que ce que vous obtenez est la liste que je veux, mais avec un élément supplémentaire au début. Alors, ça a du [-1L]sens. D'où vient ce premier élément supplémentaire, de toute façon? Et y a-t-il une raison pour laquelle je devrais utiliser ceci au lieu de simplement list(...)?
Ryan C. Thompson
6

Cela fonctionne comme prévu. Ce qui suit est une session interactive:

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

Idem, sauf avec un argument par défaut:

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

Comme vous pouvez le voir, vous pouvez l'utiliser pour passer des arguments «supplémentaires» à une fonction dans votre fonction si les valeurs par défaut ne sont pas ce que vous voulez dans un cas particulier.

Overloaded_Operator
la source