Manière «correcte» de spécifier des arguments optionnels dans les fonctions R

165

Je suis intéressé par la manière «correcte» d'écrire des fonctions avec des arguments optionnels dans R. Au fil du temps, je suis tombé sur quelques morceaux de code qui empruntent un chemin différent ici, et je n'ai pas trouvé de position (officielle) correcte sur ce sujet.

Jusqu'à présent, j'ai écrit des arguments optionnels comme celui-ci:

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

La fonction renvoie simplement son argument si seulement xest fourni. Il utilise une NULLvaleur par défaut pour le deuxième argument et si cet argument ne l'est pas NULL, la fonction ajoute les deux nombres.

Alternativement, on pourrait écrire la fonction comme ceci (où le deuxième argument doit être spécifié par son nom, mais on pourrait aussi unlist(z)ou définir à la z <- sum(...)place):

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

Personnellement, je préfère la première version. Cependant, je peux voir le bien et le mal avec les deux. La première version est un peu moins sujette aux erreurs, mais la seconde pourrait être utilisée pour incorporer un nombre arbitraire d'options.

Existe-t-il une manière "correcte" de spécifier des arguments optionnels dans R? Jusqu'à présent, j'ai choisi la première approche, mais les deux peuvent parfois se sentir un peu "hacky".

SimonG
la source
Consultez le code source pour xy.coordsvoir une approche couramment utilisée.
Carl Witthoft
5
Le code source de xy.coordsmentionné par Carl Witthoft l peut être trouvé à xy.coords
RubenLaguna

Réponses:

129

Vous pouvez également utiliser missing()pour tester si l'argument a yété fourni ou non :

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
la source
5
J'aime mieux manquer. surtout si vous avez beaucoup de valeurs par défaut NULL, vous n'aurez pas x = NULL, y = NULL, z = NULL dans la documentation de votre package
rawr
5
@rawr missing()est également plus expressif dans le sens où il "dit ce que cela signifie". De plus, il permet aux utilisateurs de transmettre une valeur NULL, là où cela a du sens!
Josh O'Brien
31
Pour moi, il y a un gros inconvénient à utiliser manquant de cette manière: lorsque vous parcourez les arguments de la fonction, vous ne pouvez plus voir quels arguments sont requis et lesquels sont des options.
hadley
3
@param x numeric; something something; @param y numeric; **optional** something something; @param z logical; **optional** something something
rawr
4
missing()est terrible lorsque vous voulez passer des arguments d'une fonction à une autre.
John Smith
55

Pour être honnête, j'aime la première façon pour l'OP de le démarrer avec une NULLvaleur, puis de la vérifier avec is.null(principalement parce que c'est très simple et facile à comprendre). Cela dépend peut-être de la façon dont les gens sont habitués au codage, mais le Hadley semble également le soutenir is.null:

Extrait du livre de Hadley "Advanced-R" Chapitre 6, Fonctions, p.84 (pour la version en ligne, vérifiez ici ):

Vous pouvez déterminer si un argument a été fourni ou non avec la fonction 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

Parfois, vous souhaitez ajouter une valeur par défaut non triviale, ce qui peut prendre plusieurs lignes de code à calculer. Au lieu d'insérer ce code dans la définition de fonction, vous pouvez utiliser missing () pour le calculer conditionnellement si nécessaire. Cependant, cela rend difficile de savoir quels arguments sont requis et lesquels sont facultatifs sans lire attentivement la documentation. Au lieu de cela, je règle généralement la valeur par défaut sur NULL et j'utilise is.null () pour vérifier si l'argument a été fourni.

LyzandeR
la source
2
Intéressant. Cela semble raisonnable, mais vous êtes-vous déjà demandé quels arguments d'une fonction sont requis et lesquels sont facultatifs? Je ne suis pas sûr d'avoir jamais eu cette expérience ...
Josh O'Brien
2
@ JoshO'Brien Je pense que je n'ai pas eu ce problème avec l'un ou l'autre des styles de codage pour être honnête, du moins cela n'a jamais été un problème majeur probablement à cause de la documentation ou de la lecture du code source. Et c'est pourquoi je dis principalement que c'est vraiment une question de style de codage auquel vous êtes habitué. J'utilise le NULLmoyen depuis un bon moment et c'est probablement pour cela que je suis plus habitué quand je vois les codes sources. Cela me semble plus naturel. Cela dit, comme vous dites que la base R adopte les deux approches, cela dépend vraiment des préférences individuelles.
LyzandeR
2
À présent, j'aimerais vraiment pouvoir marquer deux réponses comme correctes parce que ce à quoi je suis vraiment arrivé en utilisant les deux is.nullet en missingfonction du contexte et de la raison pour laquelle l'argument est utilisé.
SimonG
5
C'est ok @SimonG et merci :). Je conviens que les deux réponses sont très bonnes et qu'elles dépendent parfois du contexte. C'est une très bonne question et je pense que les réponses fournissent de très bonnes informations et connaissances, ce qui est de toute façon l'objectif principal ici.
LyzandeR
24

Voici mes règles de base:

Si les valeurs par défaut peuvent être calculées à partir d'autres paramètres, utilisez des expressions par défaut comme dans:

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

sinon en utilisant manquant

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

Dans le cas rare où vous pensez qu'un utilisateur peut vouloir spécifier une valeur par défaut qui dure toute une session R, utilisezgetOption

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

Si certains paramètres s'appliquent en fonction de la classe du premier argument, utilisez un générique 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 
}

À utiliser ...uniquement lorsque vous transmettez des paramètres supplémentaires à une autre fonction

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

Enfin, si vous choisissez l'utilisation ...sans passer les points sur une autre fonction, avertissez l'utilisateur que votre fonction ignore tous les paramètres inutilisés car cela peut être très déroutant sinon:

fun <- (x,...){
    params <- list(...)
    optionalParamNames <- letters
    unusedParams <- setdiff(names(params),optionalParamNames)
    if(length(unusedParams))
        stop('unused parameters',paste(unusedParams,collapse = ', '))
   blah blah blah 
}
Jthorpe
la source
l'option de la méthode s3 a été l'une des premières choses qui m'est venue à l'esprit aussi
rawr
2
Rétrospectivement, je me suis intéressé à la méthode d'attribution NULLde la signature de fonction de l'OP , car elle est plus pratique pour créer des fonctions qui s'enchaînent bien.
Jthorpe
10

Il existe plusieurs options et aucune d'elles n'est la manière officielle correcte et aucune d'elles n'est vraiment incorrecte, bien qu'elles puissent transmettre des informations différentes à l'ordinateur et aux autres lisant votre code.

Pour l'exemple donné, je pense que l'option la plus claire serait de fournir une valeur d'identité par défaut, dans ce cas, faites quelque chose comme:

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

C'est la plus courte des options présentées jusqu'à présent et la brièveté peut aider à la lisibilité (et parfois même à la vitesse d'exécution). Il est clair que ce qui est retourné est la somme de x et y et vous pouvez voir que y ne reçoit pas une valeur qui sera 0 qui, une fois ajoutée à x, donnera simplement x. Évidemment, si quelque chose de plus compliqué que l'addition est utilisé, une valeur d'identité différente sera nécessaire (s'il en existe une).

Une chose que j'aime vraiment dans cette approche, c'est qu'il est clair quelle est la valeur par défaut lors de l'utilisation de la argsfonction, ou même en regardant le fichier d'aide (vous n'avez pas besoin de faire défiler les détails, c'est juste là dans l'utilisation ).

L'inconvénient de cette méthode est que lorsque la valeur par défaut est complexe (nécessitant plusieurs lignes de code), cela réduirait probablement la lisibilité d'essayer de mettre tout cela dans la valeur par défaut et les approches missingou NULLdeviendraient beaucoup plus raisonnables.

Certaines des autres différences entre les méthodes apparaîtront lorsque le paramètre est transmis à une autre fonction ou lors de l'utilisation des fonctions match.callou sys.call.

Donc, je suppose que la méthode "correcte" dépend de ce que vous prévoyez de faire avec cet argument particulier et des informations que vous voulez transmettre aux lecteurs de votre code.

Greg Snow
la source
7

J'aurais tendance à préférer utiliser NULL pour la clarté de ce qui est requis et de ce qui est facultatif. Un mot d'avertissement sur l'utilisation des valeurs par défaut qui dépendent d'autres arguments, comme suggéré par Jthorpe. La valeur n'est pas définie lorsque la fonction est appelée, mais lorsque l'argument est référencé pour la première fois! Par exemple:

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

Par contre, si vous référencez y avant de changer x:

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

C'est un peu dangereux, car il est difficile de garder une trace de ce que "y" est initialisé comme s'il n'était pas appelé au début de la fonction.

Michael Grosskopf
la source
7

Je voulais juste souligner que la sinkfonction intégrée a de bons exemples de différentes façons de définir des arguments dans une fonction:

> 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
la source
1

que dis-tu de ça?

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

Puis essayez:

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