Maneira “correta” de especificar argumentos opcionais nas funções R

165

Estou interessado em qual é a maneira "correta" de escrever funções com argumentos opcionais em R. Com o tempo, deparei-me com alguns trechos de código que seguem uma rota diferente aqui e não consegui encontrar uma posição (oficial) adequada neste tópico.

Até agora, escrevi argumentos opcionais como este:

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

A função simplesmente retorna seu argumento se apenas xfor fornecida. Ele usa um NULLvalor padrão para o segundo argumento e, se esse argumento não for NULL, a função adicionará os dois números.

Como alternativa, pode-se escrever a função dessa maneira (onde o segundo argumento precisa ser especificado pelo nome, mas também pode-se unlist(z)ou definir 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

Pessoalmente, prefiro a primeira versão. No entanto, posso ver o bem e o mal com ambos. A primeira versão é um pouco menos propensa a erros, mas a segunda poderia ser usada para incorporar um número arbitrário de opcionais.

Existe uma maneira "correta" de especificar argumentos opcionais no R? Até agora, decidi pela primeira abordagem, mas ambas podem ocasionalmente parecer um pouco "hacky".

SimonG
fonte
Confira o código-fonte para xy.coordsver uma abordagem comumente usada.
Carl Witthoft
5
O código fonte xy.coordsmencionado por Carl Witthoft l pode ser encontrado em xy.coords
RubenLaguna 30/10

Respostas:

129

Você também pode usar missing()para testar se o argumento yfoi ou não fornecido:

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
fonte
5
Eu gosto mais de sentir falta. especialmente se você tem um monte de valores NULL padrão, você não terá x = NULL, y = NULL, z = NULL na documentação do pacote
rawr
5
O @rawr missing()também é mais expressivo no sentido em que "diz o que significa". Além disso, permite que os usuários passem um valor NULL, em locais onde isso faz sentido!
Josh O'Brien
31
Para mim, há uma grande desvantagem em usar a falta dessa maneira: ao percorrer os argumentos da função, você não pode mais ver quais argumentos são necessários e quais são as opções.
23415
3
@param x numeric; something something; @param y numeric; **optional** something something; @param z logical; **optional** something something
7119
4
missing()é terrível quando você deseja passar argumentos de uma função para outra.
John Smith
55

Para ser sincero, gosto da primeira maneira do OP de realmente começar com um NULLvalor e depois checá-lo is.null(principalmente porque é muito simples e fácil de entender). Talvez dependa da maneira como as pessoas estão acostumadas a codificar, mas o Hadley parece também apoiar o is.nullcaminho:

Do livro de Hadley "Advanced-R", capítulo 6, Funções, pág. 84 (para a versão online, verifique aqui ):

Você pode determinar se um argumento foi fornecido ou não com a função 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

Às vezes, você deseja adicionar um valor padrão não trivial, que pode levar várias linhas de código para calcular. Em vez de inserir esse código na definição da função, você poderia usar o ausente () para computá-lo condicionalmente, se necessário. No entanto, isso torna difícil saber quais argumentos são necessários e quais são opcionais sem ler atentamente a documentação. Em vez disso, normalmente defino o valor padrão como NULL e uso is.null () para verificar se o argumento foi fornecido.

LyzandeR
fonte
2
Interessante. Isso parece razoável, mas você já se sentiu perplexo com relação a quais argumentos para uma função são necessários e quais são opcionais? Eu não tenho certeza que eu nunca realmente tive essa experiência ...
Josh O'Brien
2
@ JoshO'Brien Acho que não tive esse problema com o estilo de codificação para ser honesto, pelo menos nunca foi um grande problema provavelmente por causa da documentação ou pela leitura do código-fonte. E é por isso que digo principalmente que é realmente uma questão do estilo de codificação que você está acostumado. Estou usando o NULLcaminho há um bom tempo e, provavelmente, é por isso que estou mais acostumado quando vejo códigos-fonte. Parece mais natural para mim. Dito isto, como você diz que a base R adota as duas abordagens, realmente se resume às preferências individuais.
LyzandeR
2
Até agora, eu realmente gostaria de poder marcar duas respostas como corretas, porque o que realmente cheguei a usar as duas is.nulle missingdependendo do contexto e para que o argumento é usado.
precisa saber é o seguinte
5
Tudo bem, @SimonG e obrigado :). Concordo que ambas as respostas são muito boas e, às vezes, dependem do contexto. Essa é uma pergunta muito boa e acredito que as respostas fornecem informações e conhecimentos muito bons, que é o principal objetivo aqui.
LyzandeR
24

Estas são minhas regras práticas:

Se os valores padrão puderem ser calculados a partir de outros parâmetros, use expressões padrão como em:

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

se não estiver usando falta

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

No caso raro de você achar que um usuário pode especificar um valor padrão que dure uma sessão R inteira, usegetOption

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

Se alguns parâmetros se aplicarem, dependendo da classe do primeiro argumento, use um S3 genérico:

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 
}

Use ...somente quando estiver passando parâmetros adicionais para outra função

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

Por fim, se você escolher o uso ...sem passar os pontos para outra função, avise o usuário que sua função está ignorando quaisquer parâmetros não utilizados, pois pode ser muito confuso caso contrário:

fun <- (x,...){
    params <- list(...)
    optionalParamNames <- letters
    unusedParams <- setdiff(names(params),optionalParamNames)
    if(length(unusedParams))
        stop('unused parameters',paste(unusedParams,collapse = ', '))
   blah blah blah 
}
Jthorpe
fonte
a opção método s3 foi uma das primeiras coisas que vieram à mente para mim, também
rawr
2
Em retrospecto, eu me apaixonei pelo método de atribuição do OP NULLna assinatura da função, pois é mais conveniente para criar funções que se encaixam perfeitamente.
precisa saber é
10

Existem várias opções e nenhuma delas é a maneira correta oficial e nenhuma delas está realmente incorreta, embora possam transmitir informações diferentes para o computador e para outras pessoas que estão lendo seu código.

Para o exemplo dado, acho que a opção mais clara seria fornecer um valor padrão de identidade, neste caso, faça algo como:

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

Essa é a menor das opções mostradas até agora, e a falta pode ajudar na legibilidade (e às vezes até na velocidade da execução). É claro que o que está sendo retornado é a soma de xey, e você pode ver que y não recebe um valor que será 0 que, quando adicionado a x, resultará apenas em x. Obviamente, se algo mais complicado que a adição for usado, será necessário um valor de identidade diferente (se houver).

Uma coisa que eu realmente gosto nessa abordagem é que fica claro qual é o valor padrão ao usar a argsfunção ou até mesmo olhar para o arquivo de ajuda (você não precisa rolar para baixo para os detalhes, está ali no uso )

A desvantagem desse método é que quando o valor padrão é complexo (exigindo várias linhas de código), provavelmente reduziria a legibilidade para tentar colocar tudo isso no valor padrão e as abordagens missingou NULLse tornam muito mais razoáveis.

Algumas das outras diferenças entre os métodos aparecerão quando o parâmetro estiver sendo transmitido para outra função ou ao usar as funções match.callou sys.call.

Portanto, acho que o método "correto" depende do que você planeja fazer com esse argumento específico e das informações que deseja transmitir aos leitores do seu código.

Greg Snow
fonte
7

Eu tenderia a preferir usar NULL para ter clareza do que é necessário e do que é opcional. Uma palavra de aviso sobre o uso de valores padrão que dependem de outros argumentos, conforme sugerido por Jthorpe. O valor não é definido quando a função é chamada, mas quando o argumento é referenciado pela primeira vez! Por exemplo:

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

Por outro lado, se você referenciar y antes de alterar x:

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

Isso é um pouco perigoso, porque torna difícil acompanhar o que "y" está sendo inicializado como se não fosse chamado no início da função.

Michael Grosskopf
fonte
7

Só queria ressaltar que a sinkfunção interna tem bons exemplos de diferentes maneiras de definir argumentos em uma função:

> 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
fonte
1

que tal agora?

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

Então tente:

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