Jak korzystać z funkcji wielokropka R podczas pisania własnej funkcji?

186

Język R ma ciekawą funkcję do definiowania funkcji, które mogą przyjmować zmienną liczbę argumentów. Na przykład funkcja data.frameprzyjmuje dowolną liczbę argumentów, a każdy argument staje się danymi dla kolumny w wynikowej tabeli danych. Przykładowe użycie:

> 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

Podpis funkcji zawiera wielokropek, taki jak ten:

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

Chciałbym napisać funkcję, która robi coś podobnego, biorąc wiele wartości i konsolidując je w jedną wartość zwracaną (a także wykonując inne przetwarzanie). Aby to zrobić, muszę dowiedzieć się, jak „rozpakować” ...argumenty funkcji w funkcji. Nie wiem jak to zrobić. Odpowiednią linią w definicji funkcji data.framejest object <- as.list(substitute(list(...)))[-1L], której nie rozumiem.

Jak mogę przekonwertować wielokropek z podpisu funkcji na, na przykład, listę?

Mówiąc ściślej, jak mogę napisać get_list_from_ellipsisw poniższym kodzie?

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)

Edytować

Wydaje się, że istnieją dwa możliwe sposoby, aby to zrobić. Oni są as.list(substitute(list(...)))[-1L]i list(...). Jednak ci dwaj nie robią dokładnie tego samego. (Aby zapoznać się z różnicami, zobacz przykłady w odpowiedziach.) Czy ktoś może mi powiedzieć, jaka jest praktyczna różnica między nimi, a której powinienem użyć?

Ryan C. Thompson
źródło

Odpowiedzi:

113

Czytam odpowiedzi i komentarze i widzę, że kilka rzeczy nie zostało wspomnianych:

  1. data.frameużywa list(...)wersji. Fragment kodu:

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

    objectsłuży do robienia magii z nazwami kolumn, ale xsłuży do tworzenia ostatecznego data.frame.
    Aby użyć nieocenionego ...argumentu, spójrz na write.csvkod, w którym match.calljest używany.

  2. Gdy piszesz w komentarzu, wynik w Dirku nie jest listą. Jest listą o długości 4, której elementami są languagetypy. Pierwszy obiekt to symbol- list, drugi to wyrażenie 1:10i tak dalej. To wyjaśnia, dlaczego [-1L]jest potrzebne: usuwa oczekiwane symbolz podanych argumentów w ...(ponieważ zawsze jest to lista).
    Jak stwierdza Dirk, substitutezwraca „parsowanie drzewa nieocenionego wyrażenia”.
    Podczas rozmowy telefonicznej my_ellipsis_function(a=1:10,b=11:20,c=21:30)następnie ...„tworzy” listę argumentów: list(a=1:10,b=11:20,c=21:30)a substitutesprawiają, że listę czterech elementów:

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

    Pierwszy element nie ma nazwy i jest to [[1]]odpowiedź Dirka. Osiągam te wyniki za pomocą:

    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. Jak wyżej, możemy użyć strdo sprawdzenia, które obiekty są w funkcji.

    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 

    W porządku. Pozwala zobaczyć substitutewersję:

       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 

    To nie jest to, czego potrzebowaliśmy. Będziesz potrzebował dodatkowych sztuczek, aby poradzić sobie z tego rodzaju obiektami (jak w write.csv).

Jeśli chcesz użyć ..., powinieneś użyć go jak w odpowiedzi Shane'a przez list(...).

Marek
źródło
38

Możesz przekonwertować wielokropek na listę za pomocą list(), a następnie wykonać na nim swoje operacje:

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

$b
[1] "numeric"

Więc twoja get_list_from_ellipsisfunkcja jest niczym więcej niż list.

Prawidłowym przypadkiem użycia jest przypadek, w którym chcesz przekazać nieznaną liczbę obiektów do działania (jak w przykładzie c()lub data.frame()). ...Jednak nie jest dobrym pomysłem, aby znać każdy parametr z góry, ponieważ dodaje on niejasności i dalszej komplikacji do łańcucha argumentów (i sprawia, że ​​podpis funkcji jest niejasny dla każdego innego użytkownika). Lista argumentów jest ważną dokumentacją dla użytkowników funkcji.

W przeciwnym razie przydaje się również w przypadkach, gdy chcesz przekazać parametry do podfunkcji bez ujawniania ich wszystkich we własnych argumentach funkcji. Można to zauważyć w dokumentacji funkcji.

Shane
źródło
Wiem o używaniu elipsy jako przekazu argumentów do podfunkcji, ale powszechną praktyką wśród prymitywów R jest używanie elipsy w sposób, który opisałem. W rzeczywistości obie funkcje listi cdziałają w ten sposób, ale obie są prymitywami, więc nie mogę łatwo sprawdzić ich kodu źródłowego, aby zrozumieć, jak działają.
Ryan C. Thompson,
rbind.data.frameużyj tego sposobu.
Marek
5
Jeśli list(...)jest wystarczające, dlaczego wbudowane R, takie jak zamiast tego data.frameużywają dłuższej formy as.list(substitute(list(...)))[-1L]?
Ryan C. Thompson,
1
Jak ja nie stworzył data.frame, nie znam odpowiedzi na to pytanie (który powiedział, jestem pewien, że nie jest to dobry powód, dla niego). Używam list()do tego celu w swoich własnych pakietach i jeszcze nie mam z tym problemu.
Shane
34

Wystarczy dodać do odpowiedzi Shane'a i Dirka: interesujące jest porównanie

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

z

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

W tej chwili każda z wersji wydaje się odpowiednia do twoich celów my_ellipsis_function, choć pierwsza jest wyraźnie prostsza.

Richie Cotton
źródło
15

Dałeś już połowę odpowiedzi. Rozważać

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> 

Wzięło to dwa argumenty ai bwywołanie i przekonwertowało je na listę. Czy nie o to prosiłeś?

Dirk Eddelbuettel
źródło
2
Nie do końca to, czego chcę. To wydaje się zwracać listę list. Zwróć uwagę na [[1]]. Chciałbym również wiedzieć, jak działa magiczna inkantacja as.list(substitute(list(...))).
Ryan C. Thompson
2
Wewnętrzne list(...)tworzy listobiekt na podstawie argumentów. Następnie substitute()tworzy drzewo analizy dla nieocenionego wyrażenia; zobacz pomoc dotyczącą tej funkcji. Jak również dobry zaawansowany tekst na R (lub S). To nie jest banalne.
Dirk Eddelbuettel
Ok, a co z tą [[-1L]]częścią (z mojego pytania)? Nie powinno tak być [[1]]?
Ryan C. Thompson
3
Musisz przeczytać o indeksowaniu. Minus oznacza „wyklucz”, tzn. print(c(1:3)[-1])Wydrukuje tylko 2 i 3. LJest nowy-fangled sposób, aby zapewnić, że kończy się jako liczbę całkowitą, odbywa się to dużo w źródłach R.
Dirk Eddelbuettel
7
I nie trzeba czytać na indeksowanie, ale nie muszą zwracać większą uwagę na wyjściu z poleceniami, które można pokazać. Różnica między indeksami [[1]]a $awskaźnikami sprawiła, że ​​pomyślałem, że w grę wchodziły zagnieżdżone listy. Ale teraz widzę, że tak naprawdę to lista, której chcę, ale z dodatkowym elementem z przodu. Więc to [-1L]ma sens. Skąd w ogóle pochodzi ten dodatkowy pierwszy element? Czy jest jakiś powód, dla którego powinienem używać tego zamiast po prostu list(...)?
Ryan C. Thompson
6

Działa to zgodnie z oczekiwaniami. Oto sesja interaktywna:

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

To samo, z wyjątkiem domyślnego argumentu:

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

Jak widać, możesz użyć tego do przekazania „dodatkowych” argumentów do funkcji w obrębie funkcji, jeśli wartości domyślne nie są takie, jakie chcesz w konkretnym przypadku.

Overloaded_Operator
źródło