Bagaimana cara menggunakan fitur ellipsis R saat menulis fungsi Anda sendiri?

186

Bahasa R memiliki fitur bagus untuk mendefinisikan fungsi yang dapat mengambil sejumlah variabel argumen. Misalnya, fungsi data.framemengambil sejumlah argumen, dan setiap argumen menjadi data untuk kolom dalam tabel data yang dihasilkan. Contoh penggunaan:

> 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

Tanda tangan fungsi mencakup elipsis, seperti ini:

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

Saya ingin menulis fungsi yang melakukan sesuatu yang serupa, mengambil beberapa nilai dan mengkonsolidasikannya ke dalam nilai pengembalian tunggal (serta melakukan beberapa pemrosesan lainnya). Untuk melakukan ini, saya perlu mencari cara untuk "membongkar" ...dari argumen fungsi di dalam fungsi. Saya tidak tahu bagaimana melakukan ini. Baris yang relevan dalam definisi fungsi data.frameadalah object <- as.list(substitute(list(...)))[-1L], yang saya tidak bisa memahaminya.

Jadi bagaimana saya bisa mengubah elipsis dari tanda tangan fungsi menjadi, misalnya, daftar?

Untuk lebih spesifik, bagaimana saya bisa menulis get_list_from_ellipsisdalam kode di bawah ini?

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)

Edit

Tampaknya ada dua cara yang memungkinkan untuk melakukan ini. Mereka as.list(substitute(list(...)))[-1L]dan list(...). Namun, keduanya tidak melakukan hal yang persis sama. (Untuk perbedaan, lihat contoh dalam jawaban.) Adakah yang bisa memberi tahu saya apa perbedaan praktis di antara mereka, dan mana yang harus saya gunakan?

Ryan C. Thompson
sumber

Jawaban:

113

Saya membaca jawaban dan komentar dan saya melihat bahwa beberapa hal tidak disebutkan:

  1. data.framemenggunakan list(...)versi. Fragmen kode:

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

    objectdigunakan untuk melakukan beberapa sihir dengan nama kolom, tetapi xdigunakan untuk membuat final data.frame.
    Untuk penggunaan ...argumen yang tidak dievaluasi, lihat write.csvkode di mana match.calldigunakan.

  2. Saat Anda menulis dalam hasil komentar dalam jawaban Dirk bukan daftar daftar. Adalah daftar panjang 4, yang elemennya adalah languagetipe. Objek pertama adalah symbol- list, kedua adalah ekspresi 1:10dan seterusnya. Itu menjelaskan mengapa [-1L]diperlukan: itu menghapus diharapkan symboldari argumen yang disediakan di ...(karena selalu daftar).
    Saat Dirk menyatakan substitutemengembalikan "parse tree ekspresi yang tidak dievaluasi".
    Ketika Anda menelepon my_ellipsis_function(a=1:10,b=11:20,c=21:30)kemudian ..."menciptakan" daftar argumen: list(a=1:10,b=11:20,c=21:30)dan substitutemembuat daftar empat unsur:

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

    Elemen pertama tidak memiliki nama dan ini ada [[1]]dalam jawaban Dirk. Saya mencapai hasil ini menggunakan:

    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. Seperti di atas kita bisa gunakan struntuk memeriksa objek apa yang ada dalam suatu fungsi.

    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 
    

    Tidak apa-apa. Mari kita lihat substituteversi:

       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 
    

    Bukan itu yang kami butuhkan. Anda akan membutuhkan trik tambahan untuk menangani objek semacam ini (seperti pada write.csv).

Jika Anda ingin menggunakan ...maka Anda harus menggunakannya seperti dalam jawaban Shane, oleh list(...).

Marek
sumber
38

Anda dapat mengubah elipsis menjadi daftar list(), dan kemudian melakukan operasi Anda di atasnya:

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

$b
[1] "numeric"

Jadi get_list_from_ellipsisfungsi Anda tidak lebih dari list.

Kasing penggunaan yang valid untuk ini adalah dalam kasus di mana Anda ingin meneruskan jumlah objek yang tidak dikenal untuk operasi (seperti dalam contoh Anda c()atau data.frame()). Ini bukan ide yang baik untuk menggunakan ...ketika Anda tahu setiap parameter di muka, karena menambahkan beberapa ambiguitas dan komplikasi lebih lanjut ke string argumen (dan membuat tanda tangan fungsi tidak jelas untuk pengguna lain). Daftar argumen adalah bagian penting dari dokumentasi untuk pengguna fungsi.

Jika tidak, ini juga berguna untuk kasus ketika Anda ingin melewati parameter ke suatu subfungsi tanpa memaparkan semuanya dalam argumen fungsi Anda sendiri. Ini dapat dicatat dalam dokumentasi fungsi.

Shane
sumber
Saya tahu tentang menggunakan ellipsis sebagai pass-through untuk argumen ke subfungsi, tetapi juga merupakan praktik umum di antara primitif R untuk menggunakan ellipsis seperti yang telah saya jelaskan. Sebenarnya, kedua fungsi listdan cfungsinya bekerja dengan cara ini, tetapi keduanya primitif, jadi saya tidak dapat dengan mudah memeriksa kode sumber mereka untuk memahami cara kerjanya.
Ryan C. Thompson
rbind.data.framegunakan cara ini.
Marek
5
Jika list(...)memadai, mengapa R builtin seperti data.framemenggunakan formulir yang lebih panjang as.list(substitute(list(...)))[-1L]?
Ryan C. Thompson
1
Karena saya tidak membuat data.frame, saya tidak tahu jawabannya (yang mengatakan, saya yakin bahwa ada adalah alasan yang baik untuk itu). Saya menggunakan list()untuk tujuan ini dalam paket saya sendiri dan belum menemukan masalah dengannya.
Shane
34

Hanya untuk menambahkan tanggapan Shane dan Dirk: menarik untuk dibandingkan

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

dengan

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

Sebagaimana adanya, versi mana pun tampaknya cocok untuk keperluan Anda my_ellipsis_function, meskipun yang pertama jelas lebih sederhana.

Richie Cotton
sumber
15

Anda sudah memberikan setengah jawaban. Mempertimbangkan

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> 

Jadi ini mengambil dua argumen adan bdari panggilan dan mengubahnya menjadi daftar. Bukankah itu yang Anda minta?

Dirk Eddelbuettel
sumber
2
Tidak seperti yang saya inginkan. Itu sebenarnya muncul untuk mengembalikan daftar daftar. Perhatikan [[1]]. Juga, saya ingin tahu bagaimana mantra sihir as.list(substitute(list(...)))bekerja.
Ryan C. Thompson
2
Bagian dalam list(...)menciptakan listobjek berdasarkan argumen. Kemudian substitute()buat pohon parse untuk ekspresi yang tidak dievaluasi; lihat bantuan untuk fungsi ini. Serta teks lanjutan yang bagus pada R (atau S). Ini bukan hal sepele.
Dirk Eddelbuettel
Ok, bagaimana dengan [[-1L]]bagian (dari pertanyaan saya)? Bukankah seharusnya begitu [[1]]?
Ryan C. Thompson
3
Anda perlu membaca tentang pengindeksan. Tanda minus berarti 'kecualikan', yaitu hanya print(c(1:3)[-1])akan mencetak 2 dan 3 saja. Ini Ladalah cara baru untuk memastikan akhirnya menjadi bilangan bulat, ini banyak dilakukan di sumber R.
Dirk Eddelbuettel
7
Saya tidak perlu membaca tentang pengindeksan, tapi saya tidak perlu memperhatikan lebih dekat dengan output dari perintah yang Anda menunjukkan. Perbedaan antara [[1]]dan $aindeks membuat saya berpikir bahwa daftar bersarang terlibat. Tetapi sekarang saya melihat bahwa apa yang sebenarnya Anda dapatkan adalah daftar yang saya inginkan, tetapi dengan elemen tambahan di bagian depan. Maka [-1L]masuk akal. Di mana elemen tambahan pertama itu berasal? Dan adakah alasan saya harus menggunakan ini bukan hanya list(...)?
Ryan C. Thompson
6

Ini berfungsi seperti yang diharapkan. Berikut ini adalah sesi interaktif:

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

Sama, kecuali dengan argumen default:

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

Seperti yang Anda lihat, Anda bisa menggunakan ini untuk memberikan argumen 'ekstra' ke fungsi di dalam fungsi Anda jika defaultnya tidak seperti yang Anda inginkan dalam kasus tertentu.

Overloaded_Operator
sumber