Cara "Benar" untuk menentukan argumen opsional dalam fungsi R.

165

Saya tertarik pada apa cara "benar" untuk menulis fungsi dengan argumen opsional di R. Seiring waktu, saya menemukan beberapa potong kode yang mengambil rute berbeda di sini, dan saya tidak dapat menemukan posisi yang tepat (resmi). pada topik ini.

Sampai sekarang, saya telah menulis argumen opsional seperti ini:

fooBar <- function(x,y=NULL){
  if(!is.null(y)) x <- x+y
  return(x)
}
fooBar(3) # 3
fooBar(3,1.5) # 4.5

Fungsi hanya mengembalikan argumennya jika hanya xdisediakan. Itu menggunakan NULLnilai default untuk argumen kedua dan jika argumen itu kebetulan tidak NULL, maka fungsi menambahkan dua angka.

Atau, seseorang dapat menulis fungsi seperti ini (di mana argumen kedua perlu ditentukan dengan nama, tetapi orang juga dapat unlist(z)atau mendefinisikan z <- sum(...)):

fooBar <- function(x,...){
  z <- list(...)
  if(!is.null(z$y)) x <- x+z$y
  return(x)
}
fooBar(3) # 3
fooBar(3,y=1.5) # 4.5

Secara pribadi saya lebih suka versi pertama. Namun, saya bisa melihat baik dan buruk dengan keduanya. Versi pertama agak kurang rentan terhadap kesalahan, tetapi yang kedua dapat digunakan untuk memasukkan jumlah opsional yang sewenang-wenang.

Apakah ada cara "benar" untuk menentukan argumen opsional di R? Sejauh ini, saya sudah sepakat dengan pendekatan pertama, tetapi keduanya kadang-kadang bisa terasa sedikit "hacky".

SimonG
sumber
Lihat kode sumber untuk xy.coordsmelihat pendekatan yang umum digunakan.
Carl Witthoft
5
Kode sumber untuk xy.coordsdisebutkan oleh Carl Witthoft l dapat ditemukan di xy.coords
RubenLaguna

Jawaban:

129

Anda juga dapat menggunakan missing()untuk menguji apakah argumen ydiberikan atau tidak :

fooBar <- function(x,y){
    if(missing(y)) {
        x
    } else {
        x + y
    }
}

fooBar(3,1.5)
# [1] 4.5
fooBar(3)
# [1] 3
Josh O'Brien
sumber
5
Saya suka hilang lebih baik. terutama jika Anda memiliki banyak nilai default NULL, Anda tidak akan memiliki x = NULL, y = NULL, z = NULL dalam dokumentasi paket Anda
rawr
5
@rawr missing()juga lebih ekspresif dalam arti bahwa "mengatakan apa artinya". Plus itu memungkinkan pengguna untuk memberikan nilai NULL, di tempat-tempat yang masuk akal!
Josh O'Brien
31
Bagi saya, ada kerugian besar untuk menggunakan yang hilang dengan cara ini: ketika membaca sekilas argumen fungsi, Anda tidak lagi dapat melihat argumen mana yang diperlukan dan opsi mana.
Hadley
3
@param x numeric; something something; @param y numeric; **optional** something something; @param z logical; **optional** something something
rawr
4
missing()mengerikan ketika Anda ingin menyampaikan argumen dari satu fungsi ke fungsi lainnya.
John Smith
55

Sejujurnya saya suka cara pertama OP benar-benar mulai dengan NULLnilai dan kemudian memeriksanya is.null(terutama karena sangat sederhana dan mudah dimengerti). Ini mungkin tergantung pada cara orang digunakan untuk pengkodean tetapi Hadley tampaknya juga mendukung is.nullcara itu:

Dari buku Hadley "Advanced-R" Bab 6, Functions, hal.84 (untuk versi online periksa di sini ):

Anda dapat menentukan apakah argumen diberikan atau tidak dengan fungsi yang hilang ().

i <- function(a, b) {
  c(missing(a), missing(b))
}
i()
#> [1] TRUE TRUE
i(a = 1)
#> [1] FALSE  TRUE
i(b = 2)
#> [1]  TRUE FALSE
i(1, 2)
#> [1] FALSE FALSE

Terkadang Anda ingin menambahkan nilai default non-sepele, yang mungkin mengambil beberapa baris kode untuk dihitung. Alih-alih memasukkan kode itu dalam definisi fungsi, Anda bisa menggunakan missing () untuk menghitungnya secara kondisional jika diperlukan. Namun, ini membuatnya sulit untuk mengetahui argumen mana yang diperlukan dan mana yang opsional tanpa membaca dokumentasi dengan cermat. Sebagai gantinya, saya biasanya mengatur nilai default ke NULL dan menggunakan is.null () untuk memeriksa apakah argumen itu disertakan.

LyzandeR
sumber
2
Menarik. Kedengarannya masuk akal, tetapi apakah Anda pernah merasa bingung tentang argumen untuk fungsi yang diperlukan dan yang opsional? Saya tidak yakin bahwa saya pernah benar-benar memiliki pengalaman itu ...
Josh O'Brien
2
@ JoshO'Brien Saya pikir saya tidak punya masalah dengan gaya pengkodean apa pun untuk jujur, setidaknya itu tidak pernah menjadi masalah besar mungkin karena dokumentasi atau membaca kode sumber. Dan itulah mengapa saya terutama mengatakan bahwa itu benar-benar masalah gaya pengkodean yang biasa Anda lakukan. Saya telah menggunakan NULLcara ini cukup lama dan mungkin itu sebabnya saya lebih terbiasa ketika saya melihat kode sumber. Sepertinya lebih alami bagi saya. Yang mengatakan, seperti yang Anda katakan basis R mengambil kedua pendekatan itu, itu benar-benar turun ke preferensi individu.
LyzandeR
2
Pada saat ini, saya benar-benar berharap saya dapat menandai dua jawaban sebagai benar karena apa yang saya benar-benar gunakan keduanya is.nulldan missingtergantung pada konteks dan apa argumen yang digunakan.
SimonG
5
Tidak apa-apa @SimonG dan terima kasih :). Saya setuju bahwa kedua jawaban itu sangat baik dan terkadang tergantung pada konteks. Ini adalah pertanyaan yang sangat bagus dan saya yakin jawabannya memberikan informasi dan pengetahuan yang sangat bagus yang merupakan tujuan utama di sini.
LyzandeR
24

Ini adalah aturan praktis saya:

Jika nilai default dapat dihitung dari parameter lain, gunakan ekspresi default seperti pada:

fun <- function(x,levels=levels(x)){
    blah blah blah
}

jika sebaliknya gunakan yang hilang

fun <- function(x,levels){
    if(missing(levels)){
        [calculate levels here]
    }
    blah blah blah
}

Dalam kasus yang jarang Anda anggap pengguna mungkin ingin menentukan nilai default yang berlangsung seluruh sesi R, gunakangetOption

fun <- function(x,y=getOption('fun.y','initialDefault')){# or getOption('pkg.fun.y',defaultValue)
    blah blah blah
}

Jika beberapa parameter berlaku tergantung pada kelas argumen pertama, gunakan S3 generik:

fun <- function(...)
    UseMethod(...)


fun.character <- function(x,y,z){# y and z only apply when x is character
   blah blah blah 
}

fun.numeric <- function(x,a,b){# a and b only apply when x is numeric
   blah blah blah 
}

fun.default <- function(x,m,n){# otherwise arguments m and n apply
   blah blah blah 
}

Gunakan ...hanya ketika Anda meneruskan parameter tambahan ke fungsi lain

cat0 <- function(...)
    cat(...,sep = '')

Terakhir, jika Anda memilih penggunaan ...tanpa mengalihkan titik ke fungsi lain, ingatkan pengguna bahwa fungsi Anda mengabaikan parameter yang tidak digunakan karena bisa sangat membingungkan jika tidak:

fun <- (x,...){
    params <- list(...)
    optionalParamNames <- letters
    unusedParams <- setdiff(names(params),optionalParamNames)
    if(length(unusedParams))
        stop('unused parameters',paste(unusedParams,collapse = ', '))
   blah blah blah 
}
Jthorpe
sumber
opsi metode s3 adalah salah satu hal pertama yang terlintas di benak saya
rawr
2
Dalam retrospeksi, saya telah menjadi menyukai metode penugasan OP NULLdalam tanda tangan fungsi, karena lebih nyaman untuk membuat fungsi yang berantai dengan baik.
Jthorpe
10

Ada beberapa opsi dan tidak ada yang merupakan cara resmi yang benar dan tidak ada yang benar-benar salah, meskipun mereka dapat menyampaikan informasi yang berbeda ke komputer dan kepada orang lain yang membaca kode Anda.

Untuk contoh yang diberikan, saya pikir pilihan yang paling jelas adalah memberikan nilai default identitas, dalam hal ini lakukan sesuatu seperti:

fooBar <- function(x, y=0) {
  x + y
}

Ini adalah pilihan terpendek yang ditampilkan sejauh ini dan singkatnya dapat membantu keterbacaan (dan kadang-kadang bahkan kecepatan dalam eksekusi). Jelas bahwa apa yang dikembalikan adalah jumlah x dan y dan Anda dapat melihat bahwa y tidak diberi nilai yang akan menjadi 0 yang bila ditambahkan ke x hanya akan menghasilkan x. Jelas jika sesuatu yang lebih rumit daripada penambahan digunakan maka nilai identitas yang berbeda akan diperlukan (jika ada).

Satu hal yang saya sangat suka tentang pendekatan ini adalah jelas apa nilai default ketika menggunakan argsfungsi, atau bahkan melihat file bantuan (Anda tidak perlu gulir ke bawah ke rinciannya, itu ada di sana dalam penggunaan ).

Kelemahan dari metode ini adalah ketika nilai standarnya kompleks (membutuhkan beberapa baris kode), maka mungkin akan mengurangi keterbacaan untuk mencoba memasukkan semua itu ke dalam nilai default dan missingatau NULLpendekatannya menjadi jauh lebih masuk akal.

Beberapa perbedaan lain antara metode akan muncul ketika parameter diturunkan ke fungsi lain, atau saat menggunakan fungsi match.callatau sys.call.

Jadi saya kira metode "benar" tergantung pada apa yang Anda rencanakan dengan argumen khusus itu dan informasi apa yang ingin Anda sampaikan kepada pembaca kode Anda.

Greg Snow
sumber
7

Saya cenderung lebih suka menggunakan NULL untuk kejelasan tentang apa yang diperlukan dan apa yang opsional. Satu kata peringatan tentang penggunaan nilai default yang bergantung pada argumen lain, seperti yang disarankan oleh Jthorpe. Nilai tidak diatur ketika fungsi dipanggil, tetapi ketika argumen pertama kali direferensikan! Misalnya:

foo <- function(x,y=length(x)){
    x <- x[1:10]
    print(y)
}
foo(1:20) 
#[1] 10

Di sisi lain, jika Anda merujuk y sebelum mengubah x:

foo <- function(x,y=length(x)){
    print(y)
    x <- x[1:10]
}
foo(1:20) 
#[1] 20

Ini agak berbahaya, karena membuatnya sulit untuk melacak apa yang "y" diinisialisasi seolah-olah itu tidak dipanggil sejak awal dalam fungsi.

Michael Grosskopf
sumber
7

Hanya ingin menunjukkan bahwa sinkfungsi bawaan memiliki contoh bagus berbagai cara untuk mengatur argumen dalam suatu fungsi:

> sink
function (file = NULL, append = FALSE, type = c("output", "message"),
    split = FALSE)
{
    type <- match.arg(type)
    if (type == "message") {
        if (is.null(file))
            file <- stderr()
        else if (!inherits(file, "connection") || !isOpen(file))
            stop("'file' must be NULL or an already open connection")
        if (split)
            stop("cannot split the message connection")
        .Internal(sink(file, FALSE, TRUE, FALSE))
    }
    else {
        closeOnExit <- FALSE
        if (is.null(file))
            file <- -1L
        else if (is.character(file)) {
            file <- file(file, ifelse(append, "a", "w"))
            closeOnExit <- TRUE
        }
        else if (!inherits(file, "connection"))
            stop("'file' must be NULL, a connection or a character string")
        .Internal(sink(file, closeOnExit, FALSE, split))
    }
}
pengguna5359531
sumber
1

bagaimana dengan ini?

fun <- function(x, ...){
  y=NULL
  parms=list(...)
  for (name in names(parms) ) {
    assign(name, parms[[name]])
  }
  print(is.null(y))
}

Lalu coba:

> fun(1,y=4)
[1] FALSE
> fun(1)
[1] TRUE
Keyu Nie
sumber