Forma "correcta" de especificar argumentos opcionales en funciones R


165

Estoy interesado en cuál es la forma "correcta" de escribir funciones con argumentos opcionales en R. Con el tiempo, me topé con algunas piezas de código que toman una ruta diferente aquí, y no pude encontrar una posición adecuada (oficial) sobre este tema.

Hasta ahora, he escrito argumentos opcionales 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

La función simplemente devuelve su argumento si solo xse proporciona. Utiliza un NULLvalor predeterminado para el segundo argumento y si ese argumento no lo es NULL, entonces la función agrega los dos números.

Alternativamente, se podría escribir la función de esta manera (donde el segundo argumento debe especificarse por nombre, pero también se podría unlist(z)definir o 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

Personalmente prefiero la primera versión. Sin embargo, puedo ver lo bueno y lo malo con ambos. La primera versión es un poco menos propensa a errores, pero la segunda podría usarse para incorporar un número arbitrario de opciones.

¿Hay una forma "correcta" de especificar argumentos opcionales en R? Hasta ahora, me he decidido por el primer enfoque, pero ambos ocasionalmente pueden sentirse un poco "hacky".


Consulte el código fuente para xy.coordsver un enfoque de uso común.
Carl Witthoft

55
El código fuente xy.coordsmencionado por Carl Witthoft l se puede encontrar en xy.coords
RubenLaguna

Respuestas:


129

También puede usar missing()para probar si se proporcionó o no el argumento y:

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

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

55
Me gusta perderme mejor. especialmente si tiene muchos valores predeterminados NULL, no tendrá x = NULL, y = NULL, z = NULL en la documentación del paquete
rawr

55
@rawr missing()también es más expresivo en el sentido de que "dice lo que significa". Además, permite a los usuarios pasar un valor de NULL, en lugares donde eso tiene sentido.
Josh O'Brien

31
Para mí, hay una gran desventaja en el uso de los que faltan de esta manera: al eliminar los argumentos de la función, ya no puede ver qué argumentos son necesarios y cuáles son las opciones.
Hadley

3
@param x numeric; something something; @param y numeric; **optional** something something; @param z logical; **optional** something something
rawr

44
missing()es terrible cuando quieres pasar argumentos de una función a otra.
John Smith

55

Para ser honesto, me gusta la primera forma del OP de comenzarlo con un NULLvalor y luego verificarlo is.null(principalmente porque es muy simple y fácil de entender). Tal vez depende de la forma en que las personas están acostumbradas a la codificación, pero el Hadley también parece apoyar el is.nullcamino:

Del libro de Hadley "Advanced-R" Capítulo 6, Funciones, p.84 (para la versión en línea verifique aquí ):

Puede determinar si se proporcionó un argumento o no con la función 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

A veces, desea agregar un valor predeterminado no trivial, que puede tomar varias líneas de código para calcular. En lugar de insertar ese código en la definición de la función, puede usar missing () para calcularlo condicionalmente si es necesario. Sin embargo, esto dificulta saber qué argumentos son necesarios y cuáles son opcionales sin leer detenidamente la documentación. En cambio, generalmente establezco el valor predeterminado en NULL y uso is.null () para verificar si se proporcionó el argumento.


2
Interesante. Eso suena razonable, pero ¿alguna vez te encuentras perplejo sobre qué argumentos de una función se requieren y cuáles son opcionales? No estoy seguro de que he nunca realmente tenía esa experiencia ...
Josh O'Brien

2
@ JoshO'Brien Creo que no he tenido ese problema con ninguno de los estilos de codificación para ser honesto, al menos nunca fue un problema importante probablemente debido a la documentación o la lectura del código fuente. Y es por eso que principalmente digo que realmente es una cuestión del estilo de codificación al que estás acostumbrado. He estado usando el NULLcamino durante bastante tiempo y probablemente es por eso que estoy más acostumbrado cuando veo los códigos fuente. Me parece más natural. Dicho esto, como usted dice, la base R adopta ambos enfoques, realmente se reduce a las preferencias individuales.
LyzandeR

2
A estas alturas, realmente desearía poder marcar dos respuestas como correctas porque realmente llegué a usar ambas is.nully missingdependiendo del contexto y para qué se usa el argumento.
SimonG

55
Eso está bien @SimonG y gracias :). Estoy de acuerdo en que ambas respuestas son muy buenas y que a veces dependen del contexto. Esta es una muy buena pregunta y creo que las respuestas proporcionan muy buena información y conocimiento, que es el objetivo principal aquí de todos modos.
LyzandeR

24

Estas son mis reglas generales:

Si los valores predeterminados se pueden calcular a partir de otros parámetros, use expresiones predeterminadas como en:

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

si de lo contrario usa falta

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

En el raro caso de que un usuario desee especificar un valor predeterminado que dure una sesión R completa, usegetOption

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

Si se aplican algunos parámetros según la clase del primer argumento, use un genérico 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 
}

Úselo ...solo cuando pase parámetros adicionales a otra función

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

Finalmente, si elige el uso ...sin pasar los puntos a otra función, advierta al usuario que su función ignora los parámetros no utilizados, ya que de lo contrario puede ser muy confuso:

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

la opción método s3 fue una de las primeras cosas que vienen a la mente para mí, también
rawr

2
En retrospectiva, me he aficionado al método de asignación de OP NULLen la firma de la función, ya que es más conveniente para crear funciones que encadenan bien.
Jthorpe

10

Hay varias opciones y ninguna de ellas es la forma correcta oficial y ninguna de ellas es realmente incorrecta, aunque pueden transmitir información diferente a la computadora y a otras personas que leen su código.

Para el ejemplo dado, creo que la opción más clara sería proporcionar un valor predeterminado de identidad, en este caso hacer algo como:

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

Esta es la más corta de las opciones mostradas hasta ahora y la brevedad puede ayudar a la legibilidad (y, a veces, incluso a acelerar la ejecución). Está claro que lo que se devuelve es la suma de x e y y puede ver que a y no se le da un valor de que será 0, que cuando se agrega a x solo dará como resultado x. Obviamente, si se usa algo más complicado que la suma, se necesitará un valor de identidad diferente (si existe).

Una cosa que realmente me gusta de este enfoque es que está claro cuál es el valor predeterminado al usar la argsfunción, o incluso al mirar el archivo de ayuda (no es necesario desplazarse hacia abajo para ver los detalles, está justo ahí en el uso )

El inconveniente de este método es cuando el valor predeterminado es complejo (requiere múltiples líneas de código), entonces probablemente reduciría la legibilidad para tratar de poner todo eso en el valor predeterminado y los enfoques missingo NULLse vuelven mucho más razonables.

Algunas de las otras diferencias entre los métodos aparecerán cuando el parámetro se pase a otra función, o cuando se usen las funciones match.callo sys.call.

Así que supongo que el método "correcto" depende de lo que planeas hacer con ese argumento en particular y qué información quieres transmitir a los lectores de tu código.


7

Tendería a preferir usar NULL por la claridad de lo que se requiere y lo que es opcional. Una palabra de advertencia sobre el uso de valores predeterminados que dependen de otros argumentos, como lo sugiere Jthorpe. ¡El valor no se establece cuando se llama a la función, sino cuando se hace referencia al argumento por primera vez! Por ejemplo:

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

Por otro lado, si hace referencia a y antes de cambiar x:

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

Esto es un poco peligroso, porque hace que sea difícil hacer un seguimiento de lo que se está inicializando "y" como si no se llamara al principio de la función.


7

Solo quería señalar que la sinkfunción incorporada tiene buenos ejemplos de diferentes formas de establecer argumentos en una función:

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

1

¿Qué tal esto?

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

Entonces intenta:

> fun(1,y=4)
[1] FALSE
> fun(1)
[1] TRUE
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.