Jawne wywołanie return w funkcji lub nie


199

Jakiś czas temu zostałem upomniany przez Simona Urbanka z zespołu podstawowego R (jak sądzę) za zalecenie użytkownikowi jawnego wywołania returnna końcu funkcji (jego komentarz został jednak usunięty):

foo = function() {
  return(value)
}

zamiast tego zalecił:

foo = function() {
  value
}

Prawdopodobnie w takiej sytuacji jest to wymagane:

foo = function() {
 if(a) {
   return(a)
 } else {
   return(b)
 }
}

Jego komentarz rzucił nieco światła na to, dlaczego nie dzwonić, returnchyba że jest to absolutnie potrzebne, ale jest to dobre.

Moje pytanie brzmi: dlaczego nie dzwonisz returnszybciej ani lepiej, a zatem lepiej?


12
returnjest niepotrzebny nawet w ostatnim przykładzie. Usunięcie returnmoże trochę przyspieszyć, ale moim zdaniem dzieje się tak, ponieważ mówi się, że R jest funkcjonalnym językiem programowania.
kohske

4
@kohske Czy możesz rozwinąć swój komentarz w odpowiedź, w tym więcej szczegółów na temat tego, dlaczego jest on szybszy i w jaki sposób jest to związane z funkcjonowaniem języka programowania R?
Paul Hiemstra

2
returnwywołuje skok nielokalny, a wyraźny skok nielokalny jest niezwykły dla FP. W rzeczywistości na przykład schemat nie ma return. Myślę, że moje komentarze są zbyt krótkie (i być może niepoprawne) jako odpowiedź.
kohske

2
F # nie ma return, break, continuealbo, co jest nudny czasem.
colinfang

Odpowiedzi:


128

Pytanie brzmiało: dlaczego (jawnie) wywołanie powrotu nie jest szybsze ani lepsze, a zatem preferowane?

W dokumentacji R nie ma takiego stwierdzenia.
Strona główna? „Funkcja” mówi:

function( arglist ) expr
return(value)

Czy jest to szybsze bez konieczności powrotu?

Zarówno function()i return()są funkcjami prymitywne i function()sama zwraca ostatnią wartość oceniana nawet bez włączania return()funkcji.

Wywołanie return()jak .Primitive('return')z tą ostatnią wartością jako argumentem wykona tę samą pracę, ale wymaga jeszcze jednego połączenia. Aby to (często) niepotrzebne .Primitive('return')połączenie mogło przyciągnąć dodatkowe zasoby. Prosty pomiar pokazuje jednak, że wynikowa różnica jest bardzo mała, a zatem nie może być powodem nieużywania wyraźnego zwrotu. Z danych wybranych w ten sposób tworzony jest następujący wykres:

bench_nor2 <- function(x,repeats) { system.time(rep(
# without explicit return
(function(x) vector(length=x,mode="numeric"))(x)
,repeats)) }

bench_ret2 <- function(x,repeats) { system.time(rep(
# with explicit return
(function(x) return(vector(length=x,mode="numeric")))(x)
,repeats)) }

maxlen <- 1000
reps <- 10000
along <- seq(from=1,to=maxlen,by=5)
ret <- sapply(along,FUN=bench_ret2,repeats=reps)
nor <- sapply(along,FUN=bench_nor2,repeats=reps)
res <- data.frame(N=along,ELAPSED_RET=ret["elapsed",],ELAPSED_NOR=nor["elapsed",])

# res object is then visualized
# R version 2.15

Porównanie upływu czasu funkcji

Zdjęcie powyżej może nieznacznie różnić się na twojej platformie. Na podstawie zmierzonych danych rozmiar zwracanego obiektu nie powoduje żadnej różnicy, liczba powtórzeń (nawet jeśli jest skalowana w górę) stanowi tylko bardzo małą różnicę, która w prawdziwym słowie z prawdziwymi danymi i prawdziwym algorytmem nie mogła być policzona skrypt działa szybciej.

Czy to lepiej bez dzwonienia do powrotu?

Return jest dobrym narzędziem do wyraźnego projektowania „liści” kodu, w których procedura powinna się zakończyć, wyskoczyć z funkcji i zwrócić wartość.

# here without calling .Primitive('return')
> (function() {10;20;30;40})()
[1] 40
# here with .Primitive('return')
> (function() {10;20;30;40;return(40)})()
[1] 40
# here return terminates flow
> (function() {10;20;return();30;40})()
NULL
> (function() {10;20;return(25);30;40})()
[1] 25
> 

Zależy od strategii i stylu programowania programisty, jakiego stylu używa, nie może on używać return (), ponieważ nie jest to wymagane.

Podstawowi programiści używają obu podejść, tj. z wyraźnym return () i bez niego, ponieważ możliwe jest znalezienie w źródłach funkcji „base”.

Wiele razy używana jest tylko return () (bez argumentu) zwracająca NULL w przypadkach warunkowo zatrzymujących funkcję.

Nie jest jasne, czy jest to lepsze czy nie, ponieważ zwykły użytkownik lub analityk używający R nie widzi prawdziwej różnicy.

Moim zdaniem pytanie powinno brzmieć: Czy istnieje jakiekolwiek niebezpieczeństwo w przypadku korzystania z wyraźnego zwrotu pochodzącego z wdrożenia R?

A może lepiej, że użytkownik piszący kod funkcji powinien zawsze pytać: Jaki jest efekt nieużywania jawnego powrotu (lub umieszczania obiektu, który ma zostać zwrócony jako ostatni liść gałęzi kodu) w kodzie funkcji?


4
Dzięki za bardzo dobrą odpowiedź. Wierzę, że nie ma niebezpieczeństwa w użyciu returni sprowadza się to do preferencji programisty, czy z niego korzystać, czy nie.
Paul Hiemstra

38
Szybkość returnjest naprawdę ostatnią rzeczą, o którą powinieneś się martwić.
hadley

2
Myślę, że to zła odpowiedź. Przyczyny sprowadzają się do fundamentalnego braku zgody co do wartości używania niepotrzebnych returnwywołań funkcji. Pytanie, które powinieneś zadać, nie jest tym, które zaproponujesz na końcu. Zamiast tego brzmi: „dlaczego powinienem używać redundancji return? Jakie to zapewnia? ” Jak się okazuje, odpowiedź brzmi „niewiele”, a nawet „w ogóle”. Twoja odpowiedź nie docenia tego.
Konrad Rudolph

@KonradRudolph ... powtórzyłeś to, o co pytał Paul (dlaczego wyraźny powrót jest zły). Chciałbym również wiedzieć właściwą (jedyną właściwą dla każdego) odpowiedź :). Czy zastanawiasz się nad udzieleniem wyjaśnień użytkownikom tej witryny?
Petr Matousu

1
@Dason W innym miejscu połączyłem post z Redditem wyjaśniający, dlaczego ten argument jest wadliwy w tym kontekście. Niestety wydaje się, że komentarz jest automatycznie usuwany za każdym razem. Krótka wersja jest returnjak wyraźny komentarz z napisem „zwiększ x o 1” obok fragmentu kodu x = x + 2. Innymi słowy, jego jawność jest (a) całkowicie nieistotna i (b) przekazuje błędne informacje. Ponieważ returnsemantyka w R to po prostu „porzucenie tej funkcji”. To nie znaczy tak samo jak returnw innych językach.
Konrad Rudolph

102

Jeśli wszyscy się zgodzą

  1. return nie jest konieczne na końcu ciała funkcji
  2. nieużywanie returnjest nieznacznie szybsze (zgodnie z testem @ Alana, 4,3 mikrosekundy w porównaniu z 5.1)

czy wszyscy powinniśmy przestać używać returnna końcu funkcji? Na pewno nie i chciałbym wyjaśnić dlaczego. Mam nadzieję usłyszeć, czy inni ludzie podzielają moje zdanie. I przepraszam, jeśli nie jest to prosta odpowiedź na OP, ale bardziej jak długi subiektywny komentarz.

Mój główny problem z nieużywaniem returnpolega na tym, że, jak zauważył Paul, istnieją inne miejsca w ciele funkcji, w których możesz ich potrzebować. A jeśli jesteś zmuszony użyć returngdzieś pośrodku swojej funkcji, dlaczego nie wyrazisz wszystkich returninstrukcji? Nienawidzę być niekonsekwentnego. Myślę też, że kod czyta się lepiej; można zeskanować funkcję i łatwo zobaczyć wszystkie punkty wyjścia i wartości.

Paul użył tego przykładu:

foo = function() {
 if(a) {
   return(a)
 } else {
   return(b)
 }
}

Niestety można zauważyć, że można go łatwo przepisać jako:

foo = function() {
 if(a) {
   output <- a
 } else {
   output <- b
 }
output
}

Ta ostatnia wersja jest nawet zgodna z niektórymi standardami kodowania programowania, które zalecają jedną instrukcję zwrotną na funkcję. Myślę, że lepszym przykładem może być:

bar <- function() {
   while (a) {
      do_stuff
      for (b) {
         do_stuff
         if (c) return(1)
         for (d) {
            do_stuff
            if (e) return(2)
         }
      }
   }
   return(3)
}

Byłoby o wiele trudniej przepisać przy użyciu pojedynczej instrukcji return: potrzebowałoby wielu breaks oraz skomplikowanego systemu zmiennych logicznych do ich propagowania. Wszystko po to, by powiedzieć, że reguła pojedynczego powrotu nie działa dobrze z R. Więc jeśli chcesz użyć returnw niektórych miejscach ciała swojej funkcji, dlaczego nie zachować spójności i używać jej wszędzie?

Nie sądzę, aby argument prędkości był prawidłowy. Różnica 0,8 mikrosekundy jest niczym, gdy zaczynasz patrzeć na funkcje, które faktycznie coś robią. Ostatnią rzeczą, jaką widzę, jest to, że mniej się pisze, ale hej, nie jestem leniwy.


7
+1, returnw niektórych przypadkach istnieje wyraźna potrzeba oświadczenia, jak pokazał @flodel. Alternatywnie istnieją sytuacje, w których najlepiej jest pominąć instrukcję return, np. Wiele wywołań małych funkcji. We wszystkich innych, powiedzmy 95% przypadków, tak naprawdę nie ma znaczenia, czy ktoś korzysta, returnczy nie, i sprowadza się to do preferencji. Lubię używać return, ponieważ jest ono bardziej wyraźne w tym, co masz na myśli, a zatem bardziej czytelne. Może ta dyskusja jest podobna do <-vs =?
Paul Hiemstra,

7
To traktuje R jako imperatywny język programowania, którym nie jest: jest funkcjonalnym językiem programowania. Programowanie funkcjonalne po prostu działa inaczej, a returnzwracanie wartości jest bezsensowne, na równi z pisaniem if (x == TRUE)zamiast if (x).
Konrad Rudolph

4
Przepisujesz również foojako foo <- function(x) if (a) a else b(z podziałami linii, jeśli to konieczne). Nie ma potrzeby wyraźnego zwrotu ani wartości pośredniej.
hadley

26

To ciekawa dyskusja. Myślę, że przykład @ flodel jest doskonały. Myślę jednak, że to ilustruje mój punkt (i @koshke wspomina o tym w komentarzu), który returnma sens, gdy używasz trybu rozkazującego zamiast funkcjonalnego stylu kodowania .

Nie chcę się nad tym zastanawiać, ale napisałbym footak:

foo = function() ifelse(a,a,b)

Funkcjonalny styl pozwala uniknąć zmian stanu, takich jak przechowywanie wartości output. W tym stylu returnjest nie na miejscu; foowygląda bardziej jak funkcja matematyczna.

Zgadzam się z @flodel: użycie skomplikowanego systemu zmiennych boolowskich w barbyłoby mniej jasne i bezcelowe, jeśli masz return. To, co czyni bartak podatnymi na returnoświadczenia, polega na tym, że jest napisane w trybie rozkazującym. Rzeczywiście, zmienne logiczne reprezentują zmiany „stanu”, których uniknięto w stylu funkcjonalnym.

Naprawdę trudno jest przepisać barw funkcjonalnym stylu, ponieważ jest to tylko pseudokod, ale pomysł jest mniej więcej taki:

e_func <- function() do_stuff
d_func <- function() ifelse(any(sapply(seq(d),e_func)),2,3)
b_func <- function() {
  do_stuff
  ifelse(c,1,sapply(seq(b),d_func))
}

bar <- function () {
   do_stuff
   sapply(seq(a),b_func) # Not exactly correct, but illustrates the idea.
}

whilePętla byłaby najtrudniejsza przepisać, ponieważ jest kontrolowane przez państwo do zmian a.

Utrata prędkości spowodowana wywołaniem returnjest znikoma, ale wydajność uzyskana dzięki unikaniu returni przepisywaniu w funkcjonalnym stylu jest często ogromna. Powiedzenie nowym użytkownikom, aby przestali używać, returnprawdopodobnie nie pomoże, ale wprowadzenie ich do funkcjonalnego stylu przyniesie korzyści.


@Paul returnjest konieczny w trybie rozkazującym, ponieważ często chcesz wyjść z funkcji w różnych punktach pętli. Funkcjonalny styl nie używa pętli, a zatem nie potrzebuje return. W czysto funkcjonalnym stylu ostateczne wywołanie jest prawie zawsze pożądaną wartością zwrotną.

W Pythonie funkcje wymagają returninstrukcji. Jeśli jednak zaprogramowałeś swoją funkcję w stylu funkcjonalnym, prawdopodobnie będziesz mieć tylko jedną returninstrukcję: na końcu swojej funkcji.

Korzystając z przykładu z innego postu StackOverflow, powiedzmy, że potrzebujemy funkcji, która zwróci, TRUEjeśli wszystkie wartości w danym odcinku będą xnieparzyste. Możemy użyć dwóch stylów:

# Procedural / Imperative
allOdd = function(x) {
  for (i in x) if (length(i) %% 2 == 0) return (FALSE)
  return (TRUE)
}

# Functional
allOdd = function(x) 
  all(length(x) %% 2 == 1)

W stylu funkcjonalnym zwracana wartość naturalnie spada na końcu funkcji. Ponownie wygląda bardziej jak funkcja matematyczna.

@GSee Przedstawione ostrzeżenia ?ifelsesą zdecydowanie interesujące, ale nie sądzę, aby próbowały zniechęcić do korzystania z tej funkcji. W rzeczywistości ifelsema tę zaletę, że automatycznie wektoryzuje funkcje. Na przykład rozważ nieco zmodyfikowaną wersję foo:

foo = function(a) { # Note that it now has an argument
 if(a) {
   return(a)
 } else {
   return(b)
 }
}

Ta funkcja działa dobrze, gdy length(a)jest 1, ale jeśli przepisałeś ponownie fooza pomocąifelse

foo = function (a) ifelse(a,a,b)

Teraz foodziała na dowolnej długości a. W rzeczywistości zadziałałoby nawet, gdy ajest matrycą. Zwracanie wartości tego samego kształtu co testfunkcja ułatwiająca wektoryzację, a nie problem.


Nie jest dla mnie jasne, dlaczego returnnie pasuje do funkcjonalnego stylu programowania. Niezależnie od tego, czy programuje się imperatywnie lub funkcjonalnie, na pewnym etapie funkcja lub podprogram musi coś zwrócić. Na przykład programowanie funkcjonalne w Pythonie nadal wymaga returninstrukcji. Czy mógłbyś rozwinąć więcej na ten temat.
Paul Hiemstra,

W tej sytuacji używanie ifelse(a,a,b)to mój wkurzony zwierzak. Wygląda na to, że każda linia ?ifelsekrzyczy: „nie używaj mnie zamiast if (a) {a} else b”. np. „... zwraca wartość o takim samym kształcie jak test”, „jeśli yeslub nosą za krótkie, ich elementy są poddawane recyklingowi.”, „tryb wyniku może zależeć od wartości test”, „atrybut klasy wyniku jest pobierane z testi może być nieodpowiednie dla wartości wybranych z yesi no
GSee

Przy drugim spojrzeniu foonie ma większego sensu; zawsze zwróci PRAWDA lub b. Użycie ifelsego zwróci 1 lub kilka PRAWDA i / lub 1 lub kilka bs. Początkowo myślałem, że intencją tej funkcji było powiedzenie „jeśli jakaś instrukcja jest PRAWDA, zwróć coś, w przeciwnym razie zwróć coś innego”. Nie sądzę, że powinno to być wektoryzowane, ponieważ wtedy stałoby się „zwróci elementy jakiegoś obiektu, który jest PRAWDA, a dla wszystkich elementów, które nie są b
PRAWDĄ

22

Wydaje się, że bez return()tego jest szybszy ...

library(rbenchmark)
x <- 1
foo <- function(value) {
  return(value)
}
fuu <- function(value) {
  value
}
benchmark(foo(x),fuu(x),replications=1e7)
    test replications elapsed relative user.self sys.self user.child sys.child
1 foo(x)     10000000   51.36 1.185322     51.11     0.11          0         0
2 fuu(x)     10000000   43.33 1.000000     42.97     0.05          0         0

____ EDYCJA__ _ __ _ __ _ __ _ __ _ ___

Przechodzę do innych testów porównawczych ( benchmark(fuu(x),foo(x),replications=1e7)) i wynik jest odwrócony ... Spróbuję na serwerze.


Czy mógłbyś skomentować przyczynę tej różnicy?
Paul Hiemstra

4
@PaulHiemstra Petr's Answer obejmuje jeden z głównych powodów tego; dwa połączenia podczas korzystania return(), jedno jeśli nie. Jest całkowicie redundantny na końcu funkcji, ponieważ function()zwraca jego ostatnią wartość. Zauważysz to tylko w wielu powtórzeniach funkcji, w których niewiele robi się wewnętrznie, tak że koszt return()staje się dużą częścią całkowitego czasu obliczeniowego funkcji.
Gavin Simpson,

13

Problem z tym, że nie umieszcza się wyraźnie „return” na końcu, polega na tym, że jeśli dodaje się dodatkowe instrukcje na końcu metody, nagle zwracana wartość jest błędna:

foo <- function() {
    dosomething()
}

Zwraca wartość dosomething().

Teraz przychodzimy następnego dnia i dodajemy nową linię:

foo <- function() {
    dosomething()
    dosomething2()
}

Chcieliśmy, aby nasz kod zwrócił wartość dosomething(), ale zamiast tego już tego nie robi.

Dzięki wyraźnemu zwrotowi staje się to naprawdę oczywiste:

foo <- function() {
    return( dosomething() )
    dosomething2()
}

Widzimy, że w tym kodzie jest coś dziwnego i napraw go:

foo <- function() {
    dosomething2()
    return( dosomething() )
}

1
tak, faktycznie uważam, że jawne return () jest przydatne podczas debugowania; po wyczyszczeniu kodu jego potrzeba jest mniej przekonująca i wolę elegancję, że go nie mam ...
PatrickT

Ale tak naprawdę nie jest to problem w prawdziwym kodzie, jest to czysto teoretyczny. Kod, który może cierpieć z tego powodu, ma znacznie większy problem: niejasny, kruchy przepływ kodu, który jest tak nieoczywisty, że psują go proste dodatki.
Konrad Rudolph

@KonradRudolph Wydaje mi się, że robisz na nim Nie-prawdziwego Szkota ;-) „Jeśli jest to problem w kodzie, jesteś złym programistą!”. Naprawdę się nie zgadzam. Myślę, że chociaż można uniknąć skrótów do małych fragmentów kodu, w których każdy wiersz zna się na pamięć, to wróci do gryzienia, gdy twój kod się powiększy.
Hugh Perkins

2
@HughPerkins To nie jest prawdziwy Szkot ; jest to raczej obserwacja empiryczna na temat złożoności kodu, poparta dziesięcioleciami najlepszych praktyk inżynierii oprogramowania: utrzymuj krótkie funkcje poszczególnych funkcji i oczywisty przepływ kodu. Pominięcie returnnie jest skrótem, to właściwy styl programowania funkcjonalnego. Używanie niepotrzebnych returnwywołań funkcji jest przykładem programowania kultu .
Konrad Rudolph

Cóż ... Nie rozumiem, jak to uniemożliwia dodanie czegoś po returnwyciągu i niezauważenie, że nie zostanie wykonane. dosomething() # this is my return value, don't add anything after it unless you know goddam well what you are doing
Równie

10

Moje pytanie brzmi: dlaczego nie dzwonisz returnszybciej

Jest szybszy, ponieważ returnw R jest funkcją (pierwotną), co oznacza, że ​​użycie jej w kodzie pociąga za sobą koszt wywołania funkcji. Porównaj to z większością innych języków programowania, w których returnjest słowem kluczowym, ale nie wywołaniem funkcji: nie przekłada się to na wykonanie kodu wykonawczego.

To powiedziawszy, wywoływanie funkcji prymitywnej w ten sposób jest dość szybkie w R, a wywoływanie returnpowoduje niewielki narzut. To nie jest argument za pominięciem return.

lub lepiej, a zatem lepiej?

Ponieważ nie ma powodu, aby z niego korzystać.

Ponieważ jest redundantny i nie dodaje użytecznej redundancji.

Żeby było jasne: nadmiarowość może czasem być przydatna . Ale większość redundancji nie jest tego rodzaju. Zamiast tego jest to rodzaj wizualnego bałaganu bez dodawania informacji: jest to programowy odpowiednik słowa wypełniającego lub chartjunk ).

Rozważ następujący przykład komentarza wyjaśniającego, który jest powszechnie uznawany za złą redundancję, ponieważ komentarz ten jedynie parafrazuje to, co już wyraża kod:

# Add one to the result
result = x + 1

Używanie returnw R należy do tej samej kategorii, ponieważ R jest funkcjonalnym językiem programowania , aw R każde wywołanie funkcji ma wartość . Jest to podstawowa właściwość R. A gdy zobaczysz kod R z perspektywy, że każde wyrażenie (w tym każde wywołanie funkcji) ma wartość, powstaje pytanie: „dlaczego powinienem używać return?” Musi być pozytywny powód, ponieważ domyślnie nie używa się go.

Jednym z takich pozytywnych powodów jest zasygnalizowanie wczesnego wyjścia z funkcji, powiedzmy w klauzuli ochronnej :

f = function (a, b) {
    if (! precondition(a)) return() # same as `return(NULL)`!
    calculation(b)
}

Jest to prawidłowe, niepotrzebne użycie return. Jednak takie klauzule ochronne są rzadkie w języku R w porównaniu do innych języków, a ponieważ każde wyrażenie ma wartość, regularne ifnie wymaga return:

sign = function (num) {
    if (num > 0) {
        1
    } else if (num < 0) {
        -1
    } else {
        0
    }
}

Możemy nawet przepisać w fnastępujący sposób:

f = function (a, b) {
    if (precondition(a)) calculation(b)
}

… Gdzie if (cond) exprjest to samo co if (cond) expr else NULL.

Na koniec chciałbym uprzedzić trzy powszechne zastrzeżenia:

  1. Niektórzy twierdzą, że użycie returndodaje przejrzystości, ponieważ sygnalizuje „ta funkcja zwraca wartość”. Ale jak wyjaśniono powyżej, każda funkcja zwraca coś w R. Myślenie o returnmarkerze zwracania wartości nie jest po prostu zbędne, ale aktywnie wprowadza w błąd .

  2. W związku z tym Zen of Python ma wspaniałą wskazówkę, której należy zawsze przestrzegać:

    Jawne jest lepsze niż niejawne.

    W jaki sposób upuszczenie nadmiarowe returnnie narusza tego? Ponieważ wartość zwracana przez funkcję w języku funkcjonalnym jest zawsze jawna: jest to jej ostatnie wyrażenie. To znowu ten sam argument na temat jawności a redundancji.

    W rzeczywistości, jeśli chcesz jawności, użyj jej, aby podświetlić wyjątek od reguły: zaznacz funkcje, które nie zwracają znaczącej wartości, które są wywoływane tylko ze względu na ich skutki uboczne (takie jak cat). Z wyjątkiem R ma lepszy niż marker returndla tego przypadku: invisible. Na przykład pisałbym

    save_results = function (results, file) {
        # … code that writes the results to a file …
        invisible()
    }
  3. Ale co z długimi funkcjami? Czy nie będzie łatwo zgubić, co jest zwracane?

    Dwie odpowiedzi: po pierwsze, niezupełnie. Zasada jest jasna: ostatnim wyrażeniem funkcji jest jej wartość. Nie ma nic do śledzenia.

    Ale co ważniejsze, problemem w długich funkcjach nie jest brak wyraźnych returnznaczników. To długość funkcji . Długie funkcje prawie (?) Zawsze naruszają zasadę pojedynczej odpowiedzialności, a nawet jeśli nie, skorzystają na rozbiciu na części w celu zwiększenia czytelności.


Może powinienem dodać, że niektórzy ludzie opowiadają się za używaniem, returnaby uczynić go bardziej podobnym do innych języków. Ale to zły argument: inne funkcjonalne języki programowania również nie używają return. Używają go tylko imperatywne języki, w których nie każde wyrażenie ma wartość.
Konrad Rudolph

Doszedłem do tego pytania z opinią, że użycie returnwsparcia jest wyraźnie lepsze, i przeczytałem twoją odpowiedź z pełną krytyką. Twoja odpowiedź skłoniła mnie do refleksji na temat tego poglądu. Myślę, że potrzeba użycia returnjawnie (przynajmniej w moim przypadku) wiąże się z potrzebą lepszej możliwości zmiany moich funkcji w późniejszym czasie. Biorąc pod uwagę, że moje funkcje mogą być po prostu zbyt skomplikowane, teraz widzę, że celem ulepszenia mojego stylu programowania byłoby dążenie do strukturyzacji kodów w celu zachowania przejrzystości bez return. Dziękuję za te refleksje i wgląd !!
Kasper Thystrup Karstensen

6

Uważam to returnza podstęp. Zasadniczo wartość ostatniego wyrażenia obliczonego w funkcji staje się wartością funkcji - i ten ogólny wzorzec występuje w wielu miejscach. Wszystkie następujące oceny do 3:

local({
1
2
3
})

eval(expression({
1
2
3
}))

(function() {
1
2
3
})()

To, returnco tak naprawdę nie oznacza, zwraca wartość (dzieje się to z nią lub bez niej), ale „przerywa” funkcję w nieregularny sposób. W tym sensie jest to najbliższy odpowiednik instrukcji GOTO w R (istnieją również break i next). Używam returnbardzo rzadko i nigdy na końcu funkcji.

 if(a) {
   return(a)
 } else {
   return(b)
 }

... można to przepisać, ponieważ if(a) a else bjest znacznie lepiej czytelne i mniej nawiasowe. returnTutaj wcale nie ma takiej potrzeby . Mój prototypowy przypadek użycia „return” byłby jak ...

ugly <- function(species, x, y){
   if(length(species)>1) stop("First argument is too long.")
   if(species=="Mickey Mouse") return("You're kidding!")
   ### do some calculations 
   if(grepl("mouse", species)) {
      ## do some more calculations
      if(species=="Dormouse") return(paste0("You're sleeping until", x+y))
      ## do some more calculations
      return(paste0("You're a mouse and will be eating for ", x^y, " more minutes."))
      }
   ## some more ugly conditions
   # ...
   ### finally
   return("The end")
   }

Zasadniczo potrzeba wielu zwrotów sugeruje, że problem jest albo brzydki, albo źle zorganizowany. G

<>

return tak naprawdę nie potrzebuje funkcji do działania: możesz jej użyć, aby wyrwać się z zestawu wyrażeń do oceny.

getout <- TRUE 
# if getout==TRUE then the value of EXP, LOC, and FUN will be "OUTTA HERE"
# .... if getout==FALSE then it will be `3` for all these variables    

EXP <- eval(expression({
   1
   2
   if(getout) return("OUTTA HERE")
   3
   }))

LOC <- local({
   1
   2
   if(getout) return("OUTTA HERE")
   3
   })

FUN <- (function(){
   1
   2
   if(getout) return("OUTTA HERE")
   3
   })()

identical(EXP,LOC)
identical(EXP,FUN)

Dzisiaj znalazłem przypadek, w którym ktoś może naprawdę potrzebować return(mój brzydki powyższy przykład jest bardzo sztuczny): załóżmy, że musisz sprawdzić, czy wartość jest NULLlub NA: w takich przypadkach zwróć pusty ciąg, w przeciwnym razie zwróć characterwartość. Ale test is.na(NULL)daje błąd, więc wygląda na to, że można to zrobić tylko z, if(is.null(x)) return("")a następnie kontynuować if(is.na(x)) ...... (Można użyć length(x)==0zamiast, is.null(x)ale nadal nie można użyć, length(x)==0 | is.na(x)jeśli xjest NULL.)
lebatsnok

1
To dlatego, że użyłeś |(wektoryzowane OR, gdzie oceniane są obie strony) zamiast ||(zwarcie OR, nie wektoryzowane, gdzie predykaty są kolejno oceniane). Rozważmy if (TRUE | stop()) print(1)versusif (TRUE || stop()) print(1)
ASAC

2

return może zwiększyć czytelność kodu:

foo <- function() {
    if (a) return(a)       
    b     
}

3
Może w tym może. Ale to nie robi tego w twoim przykładzie. Zamiast tego przesłania (a raczej komplikuje) przepływ kodu.
Konrad Rudolph

1
twoją funkcję można uprościć: foo <- function() a || b(która jest IMO bardziej czytelna; w każdym razie nie ma „czystej” czytelności, ale czytelność w czyimś przekonaniu: są ludzie, którzy twierdzą, że język asemblera jest doskonale czytelny)
lebatsnok

1

Często pojawia się tutaj argument dotyczący redundancji. Moim zdaniem nie jest to wystarczający powód do pominięcia return(). Redundancja nie jest automatycznie złą rzeczą. Przy strategicznym użyciu nadmiarowość sprawia, że ​​kod jest bardziej przejrzysty i łatwiejszy w utrzymaniu.

Rozważ ten przykład: parametry funkcji często mają wartości domyślne. Dlatego określenie wartości, która jest taka sama jak wartość domyślna, jest zbędne. Tyle że to czyni zachowanie, którego się spodziewam. Nie trzeba wyciągać strony podręcznika funkcji, aby przypomnieć sobie, jakie są ustawienia domyślne. I nie martw się, że przyszła wersja funkcji zmieni jej ustawienia domyślne.

Z znikomą karą za wykonanie połączenia return()(zgodnie z testami zamieszczonymi tutaj przez innych) sprowadza się raczej do stylu niż do dobra i zła. Aby coś było „nie tak”, musi istnieć wyraźna wada i nikt tutaj nie wykazał w zadowalający sposób, że włączenie lub pominięcie return()ma stałą wadę. Wydaje się bardzo specyficzne dla danego przypadku i dla konkretnego użytkownika.

Oto, w czym jestem.

function(){
  #do stuff
  ...
  abcd
}

Nie podobają mi się zmienne „osierocone”, jak w powyższym przykładzie. Czy abcdbędzie częścią oświadczenia, którego nie skończyłem pisać? Czy jest to pozostałość splajnu / edycji w moim kodzie i należy go usunąć? Czy przypadkowo wkleiłem / przeniosłem coś z innego miejsca?

function(){
  #do stuff
  ...
  return(abdc)
}

Natomiast ten drugi przykład pokazuje mi, że jest to zamierzona zwracana wartość, a nie jakiś wypadek lub niekompletny kod. Dla mnie ta redundancja absolutnie nie jest bezużyteczna.

Oczywiście, kiedy funkcja zostanie zakończona i działa, mógłbym usunąć zwrot. Ale usunięcie go jest samo w sobie zbędnym dodatkowym krokiem i moim zdaniem bardziej bezużyteczne niż włączenie go return()w pierwszej kolejności.

To powiedziawszy, nie używam return()w skrócie nienazwanych funkcji jednowierszowych. Tam stanowi dużą część kodu funkcji i dlatego powoduje głównie bałagan wizualny, który sprawia, że ​​kod jest mniej czytelny. Ale w przypadku większych formalnie zdefiniowanych i nazwanych funkcji używam go i prawdopodobnie nadal tak będzie.


„Czy abcd miał być częścią oświadczenia, którego nie skończyłem pisać?” - Czym różni się to jednak od innych wyrażeń, które piszesz? Myślę, że to jest sedno naszego sporu. Posiadanie zmiennej podstawy może być specyficzne w imperatywnym języku programowania, ale jest to całkowicie normalne i oczekiwane w funkcjonalnym języku programowania. Twierdzę, że problem polega po prostu na tym, że nie znasz programowania funkcjonalnego (fakt, że mówisz o „instrukcjach” zamiast o „wyrażeniach” to potwierdza).
Konrad Rudolph

Jest inaczej, ponieważ każda inna instrukcja zwykle robi coś w bardziej oczywisty sposób: jest to przypisanie, porównanie, wywołanie funkcji ... Tak, moje pierwsze kroki kodowania były w językach imperatywnych i nadal używam języków imperatywnych. Ujednolicenie wizualnych wskazówek w różnych językach (tam, gdzie pozwalają na to języki) ułatwia moją pracę. A return()in R nic nie kosztuje. Jest obiektywnie zbędny, ale bycie „bezużytecznym” jest twoim subiektywnym osądem. Zbędne i bezużyteczne niekoniecznie są synonimami. W tym miejscu się nie zgadzamy.
cymon

Nie jestem też inżynierem oprogramowania ani informatykiem. Nie czytaj zbyt wiele niuansów w moim użyciu terminologii.
cymon

Aby wyjaśnić: „Nadmiarowe i bezużyteczne niekoniecznie są synonimami. Nie zgadzamy się z tym. ” - Nie, całkowicie się z tym zgadzam i wyraźnie to podkreśliłem w mojej odpowiedzi. Redundancja może być pomocna lub nawet kluczowa . Ale należy to aktywnie wykazać, a nie założyć. Rozumiem twój argument za tym, dlaczego tak uważasz return, i chociaż nie jestem przekonany, myślę, że jest potencjalnie ważny (zdecydowanie jest to język imperatywny… wierzę, że nie tłumaczy się na języki funkcjonalne).
Konrad Rudolph
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.