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

165

Меня интересует, каков «правильный» способ написания функций с необязательными аргументами в 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? До сих пор я остановился на первом подходе, но оба могут иногда чувствовать себя немного "хакерскими".

SimonG
источник
Проверьте исходный код, xy.coordsчтобы увидеть часто используемый подход.
Карл Виттофт
5
Исходный код для xy.coordsупомянутого Carl Witthoft l можно найти по адресу xy.coords
RubenLaguna

Ответы:

129

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

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

fooBar(3,1.5)
# [1] 4.5
fooBar(3)
# [1] 3
Джош О'Брайен
источник
5
Мне нравится скучать лучше. особенно если у вас много значений по умолчанию NULL, в документации пакета не будет x = NULL, y = NULL, z = NULL
rawr
5
@rawr missing()также более выразителен в том смысле, что он «говорит, что это значит». Кроме того, он позволяет пользователям передавать значение NULL в местах, где это имеет смысл!
Джош О'Брайен
31
Для меня есть большой недостаток в использовании пропущенных таким образом: при просмотре аргументов функции вы больше не видите, какие аргументы являются обязательными, а какие - опциями.
хэдли
3
@param x numeric; something something; @param y numeric; **optional** something something; @param z logical; **optional** something something
Rawr
4
missing()ужасно, когда вы хотите передавать аргументы от одной функции к другой.
Джон Смит
55

Честно говоря, мне нравится, когда 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 (), чтобы проверить, был ли указан аргумент.

LyzandeR
источник
2
Интересный. Это звучит разумно, но вы когда-нибудь были озадачены тем, какие аргументы для функции требуются, а какие необязательны? Я не уверен, что у меня когда- либо был такой опыт ...
Джош О'Брайен,
2
@ JoshO'Brien Я думаю, у меня не было этой проблемы ни со стилем кодирования, если честно, по крайней мере, это никогда не было серьезной проблемой, вероятно, из-за документации или чтения исходного кода. И именно поэтому я прежде всего говорю, что это действительно вопрос стиля кодирования, к которому вы привыкли. Я использовал этот NULLспособ довольно давно, и, вероятно, именно поэтому я более привык к нему, когда вижу исходные коды. Это кажется более естественным для меня. Тем не менее, как вы говорите, база R использует оба подхода, так что все сводится к индивидуальным предпочтениям.
LyzandeR
2
К настоящему времени я действительно хотел бы пометить два ответа как правильные, потому что то, к чему я действительно пришел, использовало оба, is.nullи в missingзависимости от контекста и того, для чего используется аргумент.
SimonG
5
Все в порядке @SimonG и спасибо :). Я согласен, что оба ответа очень хороши, и иногда они зависят от контекста. Это очень хороший вопрос, и я считаю, что ответы дают очень хорошую информацию и знания, которые в любом случае являются основной целью.
LyzandeR
24

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

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

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 
}
Jthorpe
источник
опция метода s3 была одной из первых вещей, которая мне пришла в голову
rawr
2
Оглядываясь назад, я стал люблю методу ФПА о назначении NULLв сигнатуре функции, так как она более удобна для создания функций , которые цепные красиво.
Jthorpe
10

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

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

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

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

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

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

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

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

Грег Сноу
источник
7

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

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" инициализируется, как если бы оно не вызывалось в начале функции.

Майкл Гросскопф
источник
7

Просто хотел бы отметить, что встроенная 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))
    }
}
user5359531
источник
1

как насчет этого?

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

Тогда попробуйте:

> fun(1,y=4)
[1] FALSE
> fun(1)
[1] TRUE
Кейу Не
источник