„Prawidłowy” sposób określania opcjonalnych argumentów w funkcjach języka R.

165

Interesuje mnie jaki jest „poprawny” sposób pisania funkcji z opcjonalnymi argumentami w R. Z biegiem czasu natknąłem się na kilka fragmentów kodu, które obierają tutaj inną drogę i nie mogłem znaleźć właściwej (oficjalnej) pozycji w tym temacie.

Do tej pory pisałem opcjonalne argumenty, takie jak:

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

Funkcja po prostu zwraca swój argument, jeśli tylko xzostanie podany. Używa domyślnej NULLwartości dla drugiego argumentu, a jeśli ten argument nie jest NULL, funkcja dodaje dwie liczby.

Alternatywnie, można by napisać funkcję w ten sposób (gdzie drugi argument musi być określony przez nazwę, ale można też zamiast tego unlist(z)zdefiniować 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

Osobiście wolę pierwszą wersję. Jednak w obu przypadkach widzę dobre i złe strony. Pierwsza wersja jest trochę mniej podatna na błędy, ale druga może być użyta do włączenia dowolnej liczby opcji.

Czy istnieje „poprawny” sposób określenia opcjonalnych argumentów w języku R? Jak dotąd zdecydowałem się na pierwsze podejście, ale oba mogą czasami wydawać się nieco „zepsute”.

SimonG
źródło
Sprawdź kod źródłowy, xy.coordsaby zobaczyć powszechnie stosowane podejście.
Carl Witthoft
5
Kod źródłowy xy.coordswspomnianego przez Carla Witthofta l można znaleźć na stronie xy.coords
RubenLaguna

Odpowiedzi:

129

Możesz również użyć missing()do sprawdzenia, czy argument yzostał podany:

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
źródło
5
Wolę tęsknić bardziej. zwłaszcza jeśli masz dużo wartości domyślnych NULL, nie będziesz mieć x = NULL, y = NULL, z = NULL w dokumentacji pakietu
rawr
5
@rawr missing()jest również bardziej wyrazisty w tym sensie, że „mówi, co oznacza”. Ponadto umożliwia użytkownikom przekazywanie wartości NULL w miejscach, w których ma to sens!
Josh O'Brien
31
Dla mnie użycie braków w ten sposób ma dużą wadę: podczas przeglądania argumentów funkcji nie można już zobaczyć, które argumenty są wymagane, a które są opcjami.
Hadley
3
@param x numeric; something something; @param y numeric; **optional** something something; @param z logical; **optional** something something
rawr
4
missing()jest okropne, gdy chcesz przekazywać argumenty z jednej funkcji do drugiej.
John Smith
55

Szczerze mówiąc, podoba mi się pierwszy sposób, w jaki OP zaczyna się od NULLwartości, a następnie sprawdza ją is.null(przede wszystkim dlatego, że jest bardzo prosty i łatwy do zrozumienia). Może to zależy od sposobu, w jaki ludzie są przyzwyczajeni do kodowania, ale Hadley wydaje się również wspierać ten is.nullsposób:

Z książki Hadleya „Advanced-R” Rozdział 6, Funkcje, str. 84 (wersję online można sprawdzić tutaj ):

Możesz określić, czy argument został podany, czy nie, za pomocą funkcji 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

Czasami chcesz dodać nietrywialną wartość domyślną, której obliczenie może zająć kilka wierszy kodu. Zamiast wstawiać ten kod do definicji funkcji, w razie potrzeby można użyć metody missing () do warunkowego obliczenia tego kodu. Jednak utrudnia to ustalenie, które argumenty są wymagane, a które opcjonalne, bez uważnego przeczytania dokumentacji. Zamiast tego zwykle ustawiam wartość domyślną na NULL i używam is.null (), aby sprawdzić, czy argument został podany.

LyzandeR
źródło
2
Ciekawy. Brzmi rozsądnie, ale czy kiedykolwiek zastanawiałeś się, które argumenty funkcji są wymagane, a które opcjonalne? Nie jestem pewien, czy kiedykolwiek miałem takie doświadczenie ...
Josh O'Brien
2
@ JoshO'Brien Myślę, że szczerze mówiąc nie miałem tego problemu z żadnym stylem kodowania, przynajmniej nigdy nie był to poważny problem, prawdopodobnie z powodu dokumentacji lub czytania kodu źródłowego. I dlatego przede wszystkim mówię, że tak naprawdę jest to kwestia stylu kodowania, do którego jesteś przyzwyczajony. Korzystam z tej NULLdrogi już od jakiegoś czasu i prawdopodobnie dlatego jestem do niej bardziej przyzwyczajona, gdy widzę kody źródłowe. Wydaje mi się to bardziej naturalne. To powiedziawszy, jak mówisz, że podstawa R przyjmuje oba podejścia, tak naprawdę sprowadza się to do indywidualnych preferencji.
LyzandeR
2
Do tej pory, ja naprawdę chciałabym zaznaczyć dwie odpowiedzi prawidłowe, ponieważ to, co naprawdę osiągniętą przy zastosowaniu zarówno is.nulli missingw zależności od kontekstu i co argument stosuje.
SimonG
5
To jest ok @SimonG i dzięki :). Zgadzam się, że obie odpowiedzi są bardzo dobre i czasami zależą od kontekstu. To bardzo dobre pytanie i uważam, że odpowiedzi dostarczają bardzo dobrych informacji i wiedzy, która i tak jest tutaj głównym celem.
LyzandeR
24

Oto moje praktyczne zasady:

Jeśli wartości domyślne można obliczyć z innych parametrów, użyj domyślnych wyrażeń, jak w:

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

jeśli w przeciwnym razie użyj missing

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

W rzadkich przypadkach, gdy wydaje Ci się, że użytkownik może chcieć określić domyślną wartość, która będzie obowiązywała przez całą sesję języka R, użyjgetOption

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

Jeśli niektóre parametry mają zastosowanie w zależności od klasy pierwszego argumentu, użyj generycznego 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 
}

Używaj ...tylko wtedy, gdy przekazujesz dodatkowe parametry do innej funkcji

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

Na koniec, jeśli wybierzesz użycie ...bez przekazywania kropek do innej funkcji, ostrzeż użytkownika, że ​​twoja funkcja ignoruje wszelkie nieużywane parametry, ponieważ w przeciwnym razie może to być bardzo mylące:

fun <- (x,...){
    params <- list(...)
    optionalParamNames <- letters
    unusedParams <- setdiff(names(params),optionalParamNames)
    if(length(unusedParams))
        stop('unused parameters',paste(unusedParams,collapse = ', '))
   blah blah blah 
}
Jthorpe
źródło
opcja metody s3 była jedną z pierwszych rzeczy, które przyszły mi do głowy
rawr
2
Z perspektywy czasu spodobała mi się metoda przypisywania NULLfunkcji w OP , ponieważ jest to wygodniejsze do tworzenia ładnych łańcuchów funkcji .
Jthorpe
10

Istnieje kilka opcji i żadna z nich nie jest oficjalnie poprawna i żadna z nich nie jest naprawdę błędna, chociaż mogą przekazywać różne informacje komputerowi i innym osobom czytającym twój kod.

W podanym przykładzie myślę, że najwyraźniejszą opcją byłoby podanie domyślnej wartości tożsamości, w tym przypadku wykonaj coś takiego:

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

Jest to najkrótsza z dotychczas wyświetlonych opcji, a skrócenie może pomóc w czytelności (a czasem nawet w przyspieszeniu wykonania). Oczywiste jest, że zwracana jest suma x i y i widać, że y nie otrzymuje wartości, która będzie równa 0, co po dodaniu do x da po prostu x. Oczywiście, jeśli zostanie użyte coś bardziej skomplikowanego niż dodawanie, potrzebna będzie inna wartość tożsamości (jeśli taka istnieje).

Jedną z rzeczy, które naprawdę podoba mi się w tym podejściu, jest to, że jest jasne, jaka jest wartość domyślna podczas korzystania z argsfunkcji, a nawet patrząc na plik pomocy (nie musisz przewijać w dół do szczegółów, jest dokładnie w użyciu ).

Wadą tej metody jest to, że gdy wartość domyślna jest złożona (wymagająca wielu wierszy kodu), wówczas próba umieszczenia tego wszystkiego w wartości domyślnej prawdopodobnie zmniejszyłaby czytelność, a podejścia missinglub NULLstałyby się znacznie bardziej rozsądne.

Niektóre inne różnice między metodami pojawią się, gdy parametr jest przekazywany do innej funkcji lub podczas korzystania z funkcji match.calllub sys.call.

Więc myślę, że „poprawna” metoda zależy od tego, co planujesz zrobić z tym konkretnym argumentem i jakie informacje chcesz przekazać czytelnikom twojego kodu.

Greg Snow
źródło
7

Wolałbym używać NULL ze względu na jasność tego, co jest wymagane, a co opcjonalne. Jedno słowo ostrzeżenia o używaniu wartości domyślnych, które zależą od innych argumentów, zgodnie z sugestią Jthorpe. Wartość nie jest ustawiana, gdy funkcja jest wywoływana, ale gdy najpierw następuje odwołanie do argumentu! Na przykład:

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

Z drugiej strony, jeśli odniesiesz się do y przed zmianą x:

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

Jest to trochę niebezpieczne, ponieważ utrudnia śledzenie inicjalizacji „y”, tak jakby nie było wywoływane we wczesnej fazie funkcji.

Michael Grosskopf
źródło
7

Po prostu chciał podkreślić, że wbudowana sinkfunkcja ma dobre przykłady różnych sposobów, aby ustawić argumentów funkcji:

> 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
źródło
1

co powiesz na to?

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

Więc spróbuj:

> fun(1,y=4)
[1] FALSE
> fun(1)
[1] TRUE
Keyu Nie
źródło