"Правильный" способ указать необязательные аргументы в R-функциях

Меня интересует, что такое "правильный" способ записи функций с необязательными аргументами в R. Со временем я наткнулся на несколько фрагментов кода, которые идут по другому пути, и я не мог найти правильную (официальную) позицию по этой теме.

До сих пор я написал необязательные аргументы вроде этого:

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

Функция просто возвращает свой аргумент, если поставляется только x. Он использует значение по умолчанию NULL для второго аргумента, и если этот аргумент не является NULL, тогда функция добавляет два числа.

В качестве альтернативы можно написать такую ​​функцию (где второй аргумент нужно указать по имени, но можно также unlist(z) или определить 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

Лично я предпочитаю первую версию. Тем не менее, я вижу хорошее и плохое с обоими. Первая версия немного менее подвержена ошибкам, но вторая может быть использована для включения произвольного количества опций.

Есть ли "правильный" способ указать необязательные аргументы в R? До сих пор я остановился на первом подходе, но оба могут иногда чувствовать себя немного "взломанными".

Ответ 1

Вы также можете использовать missing() для проверки наличия или отсутствия аргумента y:

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

fooBar(3,1.5)
# [1] 4.5
fooBar(3)
# [1] 3

Ответ 2

Честно говоря, мне нравится первый способ OP запускать его со значением NULL, а затем проверять его с помощью is.null (в первую очередь потому, что это очень просто и легко понять). Возможно, это зависит от того, как люди привыкли к кодированию, но Хэдли, похоже, тоже поддерживает is.null:

Из книги Хэдли "Advanced-R" Глава 6, Функции, стр .84 (для проверки онлайн-версии здесь):

Вы можете определить, был ли предоставлен аргумент или нет с функцией missing().

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

Иногда вы хотите добавить нетривиальное значение по умолчанию, которое может занять несколько строк кода. Вместо того, чтобы вставлять этот код в определение функции, вы можете использовать missing() для условного вычисления, если это необходимо. Однако это затрудняет понимание того, какие аргументы необходимы и которые являются необязательными, без тщательного изучения документации. Вместо этого я обычно устанавливаю значение по умолчанию NULL и использую is.null(), чтобы проверить, был ли предоставлен аргумент.

Ответ 3

Это мои эмпирические правила:

Если значения по умолчанию можно вычислить из других параметров, используйте значения по умолчанию выражения как в:

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

если в противном случае использование отсутствует

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

В редком случае, когда пользователь может указать значение по умолчанию который длится весь сеанс R, используйте getOption

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

Если некоторые параметры применяются в зависимости от класса первого аргумента, используйте общий S3:

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 
}

Используйте ... только при передаче дополнительных параметров на другая функция

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

Наконец, если вы выбрали использование ... без передачи точек на другую функцию, предупредите пользователя о том, что ваша функция игнорирует любые неиспользованные параметры, поскольку это может быть очень запутанным иначе:

fun <- (x,...){
    params <- list(...)
    optionalParamNames <- letters
    unusedParams <- setdiff(names(params),optionalParamNames)
    if(length(unusedParams))
        stop('unused parameters',paste(unusedParams,collapse = ', '))
   blah blah blah 
}

Ответ 4

Есть несколько вариантов, и ни один из них не является официальным правильным способом, и ни один из них не является действительно неправильным, хотя они могут передавать различную информацию на компьютер и другим пользователям, читающим ваш код.

В данном примере я думаю, что самым ясным вариантом будет предоставление значения по умолчанию для идентификатора, в этом случае сделайте что-то вроде:

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

Это самый короткий из показанных до сих пор параметров, а краткость может помочь в удобочитаемости (а иногда и в скорости). Понятно, что возвращаемое является суммой x и y, и вы можете видеть, что y не получает значение, которое будет 0, которое при добавлении x будет просто результатом x. Очевидно, что если используется нечто более сложное, чем добавление, тогда потребуется другое значение идентификатора (если оно существует).

Мне очень нравится этот подход, так как ясно, что значение по умолчанию используется при использовании функции args или даже при просмотре файла справки (вам не нужно прокручивать список до деталей, это находится прямо там в использовании).

Недостатком этого метода является то, когда значение по умолчанию является сложным (требуется несколько строк кода), то это, вероятно, уменьшит читаемость, чтобы попытаться поместить все это в значение по умолчанию, а подходы missing или NULL станут гораздо более разумным.

Некоторые другие отличия между методами будут отображаться при передаче параметра другой функции или при использовании функций match.call или sys.call.

Итак, я думаю, что "правильный" метод зависит от того, что вы планируете делать с этим конкретным аргументом и какой информацией вы хотите передать читателям вашего кода.

Ответ 5

Я бы предпочел использовать NULL для ясности того, что требуется и что необязательно. Одно слово предупреждения об использовании значений по умолчанию, которые зависят от других аргументов, как это предлагает Джторп. Значение не задается при вызове функции, но когда аргумент сначала ссылается! Например:

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

С другой стороны, если вы ссылаетесь на y перед изменением x:

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

Это немного опасно, потому что это затрудняет отслеживание того, что инициализируется инициализацией "y", как если бы она не вызывалась раньше в функции.

Ответ 6

Просто хотелось указать, что встроенная функция sink имеет хорошие примеры различных способов установки аргументов в функции:

> 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))
    }
}