Return explizit in einer Funktion aufrufen oder nicht


199

Vor einiger Zeit wurde ich von Simon Urbanek vom R-Kernteam (glaube ich) zurechtgewiesen , weil er einem Benutzer empfohlen hatte, returnam Ende einer Funktion explizit aufzurufen (sein Kommentar wurde jedoch gelöscht):

foo = function() {
  return(value)
}

stattdessen empfahl er:

foo = function() {
  value
}

Wahrscheinlich ist in einer solchen Situation Folgendes erforderlich:

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

Sein Kommentar gab Aufschluss darüber, warum returnes gut ist, nicht anzurufen, es sei denn, dies ist unbedingt erforderlich, aber dies wurde gelöscht.

Meine Frage ist: Warum nicht returnschneller oder besser anrufen und daher vorzuziehen?


12
returnist auch im letzten Beispiel nicht notwendig. Das Entfernen returnkann es etwas schneller machen, aber meiner Ansicht nach liegt dies daran, dass R eine funktionale Programmiersprache sein soll.
Kohske

4
@kohske Könnten Sie Ihren Kommentar zu einer Antwort erweitern, einschließlich weiterer Details darüber, warum er schneller ist und wie dies damit zusammenhängt, dass R eine funktionale Programmiersprache ist?
Paul Hiemstra

2
returninduziert einen nicht lokalen Sprung, und der explizite nicht lokale Sprung ist für FP ungewöhnlich. Eigentlich hat zum Beispiel Schema nicht return. Ich denke, meine Kommentare sind als Antwort zu kurz (und möglicherweise falsch).
Kohske

2
F # nicht über return, break, continueentweder, was ermüdend manchmal ist.
Colinfang

Antworten:


128

Die Frage war: Warum ist ein (expliziter) Rückruf nicht schneller oder besser und daher vorzuziehen?

In der R-Dokumentation gibt es keine Aussage, die eine solche Annahme trifft.
Die Hauptseite? 'Funktion' sagt:

function( arglist ) expr
return(value)

Ist es schneller ohne Rückruf?

Beide function()und return()sind primitive Funktionen, und der function()selbst gibt den zuletzt ausgewerteten Wert zurück, auch ohne die return()Funktion einzuschließen.

Das Aufrufen return()wie .Primitive('return')bei diesem letzten Wert als Argument erledigt den gleichen Job, benötigt jedoch einen weiteren Aufruf. Damit dieser (oft) unnötige .Primitive('return')Aufruf zusätzliche Ressourcen beanspruchen kann. Eine einfache Messung zeigt jedoch, dass der resultierende Unterschied sehr gering ist und daher nicht der Grund dafür sein kann, keine explizite Rückgabe zu verwenden. Das folgende Diagramm wird aus den auf diese Weise ausgewählten Daten erstellt:

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

Funktionsvergleich der verstrichenen Zeit

Das Bild oben kann auf Ihrer Plattform leicht abweichen. Basierend auf den gemessenen Daten verursacht die Größe des zurückgegebenen Objekts keinen Unterschied. Die Anzahl der Wiederholungen (auch wenn sie vergrößert sind) macht nur einen sehr kleinen Unterschied, der in realen Worten mit realen Daten und realem Algorithmus nicht gezählt werden konnte oder Ihren Skript läuft schneller.

Ist es besser ohne Rückruf?

Return ist ein gutes Werkzeug, um "Blätter" von Code klar zu entwerfen, wo die Routine enden, aus der Funktion herausspringen und Wert zurückgeben soll.

# 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
> 

Es hängt von der Strategie und dem Programmierstil des Programmierers ab, welchen Stil er verwendet. Er kann kein return () verwenden, da dies nicht erforderlich ist.

R-Kernprogrammierer verwenden beide Ansätze, d. H. mit und ohne explizite return (), wie es in Quellen von 'Basis'-Funktionen zu finden ist.

Oft wird nur return () verwendet (kein Argument) und NULL zurückgegeben, wenn die Funktion bedingt gestoppt werden soll.

Es ist nicht klar, ob es besser ist oder nicht, da Standardbenutzer oder Analysten, die R verwenden, den wirklichen Unterschied nicht erkennen können.

Meiner Meinung nach sollte die Frage lauten: Besteht die Gefahr einer expliziten Rückgabe aus der R-Implementierung?

Oder, vielleicht besser, sollte der Benutzer, der Funktionscode schreibt, immer fragen: Was bewirkt es, wenn im Funktionscode keine explizite Rückgabe (oder Platzierung eines Objekts, das als letztes Blatt des Codezweigs zurückgegeben werden soll) verwendet wird?


4
Danke für eine sehr gute Antwort. Ich glaube, es besteht keine Gefahr bei der Verwendung return, und es hängt von der Präferenz des Programmierers ab, ob er es verwendet oder nicht.
Paul Hiemstra

37
Die Geschwindigkeit von returnist wirklich das Letzte, worüber Sie sich Sorgen machen sollten.
Hadley

2
Ich denke, das ist eine schlechte Antwort. Die Gründe dafür liegen in einer grundsätzlichen Uneinigkeit über den Wert der Verwendung unnötiger returnFunktionsaufrufe. Die Frage, die Sie stellen sollten , ist nicht die, die Sie am Ende vorschlagen. Stattdessen heißt es: „Warum sollte ich eine redundante verwenden return? Welchen Nutzen bietet es? “ Wie sich herausstellt, lautet die Antwort "nicht viel" oder sogar "überhaupt nichts". Ihre Antwort weiß dies nicht zu schätzen.
Konrad Rudolph

@KonradRudolph ... du hast tatsächlich wiederholt, was Paul ursprünglich gefragt hat (warum explizite Rückkehr schlecht ist). Und ich würde gerne wissen, welche Antwort richtig ist (die richtige für jeden) :). Denken Sie darüber nach, Ihre Erklärung für Benutzer dieser Website bereitzustellen?
Petr Matousu

1
@Dason Ich habe an anderer Stelle einen Reddit-Beitrag verlinkt, in dem erklärt wird, warum dieses Argument in diesem Zusammenhang fehlerhaft ist. Leider scheint der Kommentar jedes Mal automatisch entfernt zu werden. Die Kurzversion returnist wie ein expliziter Kommentar, in dem neben einem Code "x um 1 erhöhen" steht x = x + 2. Mit anderen Worten, seine Aussage ist (a) völlig irrelevant und (b) es vermittelt die falschen Informationen. Weil returndie Semantik in R rein "diese Funktion abbrechen" ist. Es bedeutet nicht dasselbe wie returnin anderen Sprachen.
Konrad Rudolph

102

Wenn alle damit einverstanden sind

  1. return ist am Ende des Körpers einer Funktion nicht erforderlich
  2. Nichtgebrauch returnist geringfügig schneller (laut @ Alans Test 4,3 Mikrosekunden gegenüber 5,1)

Sollen wir alle returnam Ende einer Funktion aufhören zu benutzen ? Das werde ich bestimmt nicht und ich möchte erklären, warum. Ich hoffe zu hören, ob andere meine Meinung teilen. Und ich entschuldige mich, wenn es keine klare Antwort auf das OP ist, sondern eher ein langer subjektiver Kommentar.

Mein Hauptproblem bei der Nichtverwendung returnist, dass es, wie Paul betonte, andere Stellen im Körper einer Funktion gibt, an denen Sie sie möglicherweise benötigen. Und wenn Sie gezwungen sind, returnirgendwo in der Mitte Ihrer Funktion zu verwenden, warum nicht alle returnAussagen explizit machen? Ich hasse es, inkonsistent zu sein. Ich denke auch, dass der Code besser liest; Man kann die Funktion scannen und leicht alle Austrittspunkte und Werte sehen.

Paulus benutzte dieses Beispiel:

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

Leider könnte man darauf hinweisen, dass es leicht umgeschrieben werden kann als:

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

Die letztere Version entspricht sogar einigen Programmiercodierungsstandards, die eine return-Anweisung pro Funktion befürworten. Ich denke, ein besseres Beispiel hätte sein können:

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

Das Umschreiben mit einer einzigen return-Anweisung wäre viel schwieriger: Es wären mehrere breaks und ein kompliziertes System boolescher Variablen erforderlich, um sie weiterzugeben. All dies, um zu sagen, dass die Single-Return-Regel bei R nicht gut funktioniert. Wenn Sie sie also an returneinigen Stellen des Körpers Ihrer Funktion verwenden müssen, warum nicht konsistent sein und sie überall verwenden?

Ich denke nicht, dass das Geschwindigkeitsargument gültig ist. Ein Unterschied von 0,8 Mikrosekunden ist nichts, wenn Sie sich Funktionen ansehen, die tatsächlich etwas bewirken. Das Letzte, was ich sehen kann, ist, dass es weniger tippt, aber hey, ich bin nicht faul.


7
+1, returnin einigen Fällen besteht ein klarer Bedarf an der Aussage, wie @flodel gezeigt hat. Alternativ gibt es Situationen, in denen eine return-Anweisung am besten weggelassen wird, z. B. viele, viele kleine Funktionsaufrufe. In allen anderen Fällen, sagen wir 95%, spielt es keine Rolle, ob man verwendet returnoder nicht, und es kommt auf die Präferenz an. Ich verwende gerne return, da es in Ihrer Bedeutung expliziter und damit lesbarer ist. Vielleicht ist diese Diskussion mit <-vs verwandt =?
Paul Hiemstra

7
Dies behandelt R als eine zwingende Programmiersprache, was es nicht ist: Es ist eine funktionale Programmiersprache. Die funktionale Programmierung funktioniert einfach anders, und die returnRückgabe eines Werts ist unsinnig, genauso wie das Schreiben if (x == TRUE)statt if (x).
Konrad Rudolph

4
Sie schreiben auch fooals foo <- function(x) if (a) a else b(mit Zeilenumbrüchen nach Bedarf). Keine explizite Rückgabe oder Zwischenwert erforderlich.
Hadley

26

Dies ist eine interessante Diskussion. Ich finde das Beispiel von @ flodel ausgezeichnet. Ich denke jedoch, dass dies meinen Punkt veranschaulicht (und @koshke erwähnt dies in einem Kommentar), returnder sinnvoll ist, wenn Sie einen Imperativ anstelle eines funktionalen Codierungsstils verwenden .

Nicht um den Punkt zu belabouren, aber ich hätte so umgeschrieben foo:

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

Ein funktionaler Stil vermeidet Statusänderungen wie das Speichern des Werts von output. In diesem Stil returnist fehl am Platz; foosieht eher aus wie eine mathematische Funktion.

Ich stimme @flodel zu: Die Verwendung eines komplizierten Systems boolescher Variablen in barwäre weniger klar und sinnlos, wenn Sie dies getan haben return. Was Aussagen barso zugänglich macht , returnist, dass sie in einem imperativen Stil geschrieben sind. In der Tat stellen die booleschen Variablen die "Zustands" -Änderungen dar, die in einem funktionalen Stil vermieden werden.

Es ist wirklich schwierig, barim funktionalen Stil umzuschreiben , weil es nur ein Pseudocode ist, aber die Idee ist ungefähr so:

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

Die whileSchleife wäre am schwierigsten umzuschreiben, da sie durch Zustandsänderungen an gesteuert wird a.

Der Geschwindigkeitsverlust, der durch einen Anruf bei verursacht wird, returnist vernachlässigbar, aber die Effizienz, die durch das Vermeiden returnund Umschreiben in einem funktionalen Stil erzielt wird, ist oft enorm. Es returnwird wahrscheinlich nicht helfen, neuen Benutzern zu sagen, dass sie die Verwendung einstellen sollen, aber es lohnt sich, sie zu einem funktionalen Stil zu führen.


@Paul returnist im imperativen Stil erforderlich, da Sie die Funktion häufig an verschiedenen Punkten in einer Schleife beenden möchten. Ein funktionaler Stil verwendet keine Schleifen und benötigt daher keine return. In einem rein funktionalen Stil ist der letzte Aufruf fast immer der gewünschte Rückgabewert.

In Python erfordern Funktionen eine returnAnweisung. Wenn Sie Ihre Funktion jedoch in einem funktionalen Stil programmiert haben, haben Sie wahrscheinlich nur eine returnAnweisung: am Ende Ihrer Funktion.

Nehmen wir an einem Beispiel aus einem anderen StackOverflow-Beitrag an, wir wollten eine Funktion, die zurückgegeben wird, TRUEwenn alle Werte in einem bestimmten Wert xeine ungerade Länge haben. Wir könnten zwei Stile verwenden:

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

In einem funktionalen Stil fällt der zurückzugebende Wert natürlich an die Enden der Funktion. Wieder sieht es eher wie eine mathematische Funktion aus.

@GSee Die darin beschriebenen Warnungen ?ifelsesind definitiv interessant, aber ich glaube nicht, dass sie versuchen, die Verwendung der Funktion zu verhindern. In der Tat ifelsehat der Vorteil der automatischen Vektorisierung von Funktionen. Betrachten Sie beispielsweise eine leicht modifizierte Version von foo:

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

Diese Funktion funktioniert gut, wenn 1 length(a)ist. Aber wenn Sie foomit einem neu geschrieben habenifelse

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

Funktioniert jetzt fooauf jeder Länge von a. In der Tat würde es sogar funktionieren, wenn aes sich um eine Matrix handelt. Die Rückgabe eines Werts in der gleichen Form wie testein Feature, das bei der Vektorisierung hilft, ist kein Problem.


Mir ist nicht klar, warum returnes nicht zu einem funktionalen Programmierstil passt. Ob man zwingend oder funktional programmiert, irgendwann muss eine Funktion oder ein Unterprogramm etwas zurückgeben. Zum Beispiel erfordert die funktionale Programmierung in Python immer noch eine returnAnweisung. Könnten Sie dies näher erläutern?
Paul Hiemstra

In dieser Situation ist die Verwendung ifelse(a,a,b)ein Haustier von mir. Es scheint, als würde jede Zeile ?ifelseschreien: "Benutze mich nicht statt if (a) {a} else b." Beispiel: "... gibt einen Wert mit der gleichen Form wie test", "zurück. Wenn yesoder wenn sie nozu kurz sind, werden ihre Elemente recycelt.", "Der Modus des Ergebnisses kann vom Wert von test", ", dem Klassenattribut des Ergebnisses, abhängen wird entnommen testund ist möglicherweise für die aus yesund no"
GSee

Auf den zweiten Blick foomacht es nicht viel Sinn; es wird immer TRUE oder zurückgeben b. Wenn Sie ifelsees verwenden, werden 1 oder mehrere TRUEs und / oder 1 oder mehrere bs zurückgegeben. Anfangs dachte ich, die Absicht der Funktion sei zu sagen: "Wenn eine Aussage WAHR ist, gib etwas zurück, andernfalls gib etwas anderes zurück." Ich denke nicht, dass das vektorisiert werden sollte, denn dann würde es "die Elemente eines Objekts zurückgeben, die WAHR sind, und für alle Elemente, die nicht WAHR sind, zurückkehren b.
GSee

22

Es scheint, dass ohne return()es schneller ist ...

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

____ BEARBEITEN__ _ __ _ __ _ __ _ __ _ ___

Ich benchmark(fuu(x),foo(x),replications=1e7)gehe zu einem anderen Benchmark ( ) und das Ergebnis ist umgekehrt ... Ich werde es auf einem Server versuchen.


Können Sie den Grund für diesen Unterschied kommentieren?
Paul Hiemstra

4
Die Antwort von @PaulHiemstra Petr behandelt einen der Hauptgründe dafür; zwei Anrufe bei Verwendung return(), einer, wenn Sie dies nicht tun. Es ist am Ende einer Funktion völlig redundant, da function()es den letzten Wert zurückgibt. Sie werden dies nur in vielen Wiederholungen einer Funktion bemerken, in denen intern nicht viel getan wird, so dass die Kosten für einen return()großen Teil der gesamten Rechenzeit der Funktion ausmachen .
Gavin Simpson

13

Ein Problem, wenn 'return' nicht explizit am Ende gesetzt wird, besteht darin, dass der Rückgabewert plötzlich falsch ist, wenn am Ende der Methode zusätzliche Anweisungen hinzugefügt werden:

foo <- function() {
    dosomething()
}

Dies gibt den Wert von zurück dosomething().

Jetzt kommen wir am nächsten Tag und fügen eine neue Zeile hinzu:

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

Wir wollten, dass unser Code den Wert von zurückgibt dosomething(), aber stattdessen nicht mehr.

Bei einer expliziten Rückkehr wird dies wirklich offensichtlich:

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

Wir können sehen, dass dieser Code etwas Seltsames hat, und das Problem beheben:

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

1
Ja, eigentlich finde ich, dass eine explizite return () beim Debuggen nützlich ist. Sobald der Code bereinigt ist, ist sein Bedarf weniger zwingend und ich bevorzuge die Eleganz, ihn nicht zu haben ...
PatrickT

In echtem Code ist dies jedoch kein Problem, sondern rein theoretisch. Code, der darunter leiden könnte, hat ein viel größeres Problem: einen obskuren, spröden Code-Fluss, der so offensichtlich ist, dass einfache Ergänzungen ihn brechen.
Konrad Rudolph

@KonradRudolph Ich denke, du machst so etwas wie einen No-True Scotsman ;-) "Wenn dies ein Problem in deinem Code ist, bist du ein schlechter Programmierer!". Ich stimme nicht wirklich zu. Ich denke, während Sie mit Abkürzungen für kleine Codeteile davonkommen können, bei denen Sie jede Zeile auswendig können, wird dies Sie beißen, wenn Ihr Code größer wird.
Hugh Perkins

2
@ HughPerkins Es ist kein wahrer Schotte ; Vielmehr handelt es sich um eine empirische Beobachtung der Codekomplexität, die auf jahrzehntelangen bewährten Methoden der Softwareentwicklung beruht: Halten Sie einzelne Funktionen kurz und den Codefluss offensichtlich. Und das Weglassen returnist keine Abkürzung, sondern der richtige Stil in der funktionalen Programmierung. Die Verwendung unnötiger returnFunktionsaufrufe ist ein Beispiel für die Frachtkultprogrammierung .
Konrad Rudolph

Nun ... ich verstehe nicht, wie dies Sie daran hindert, etwas nach Ihrer returnAnweisung hinzuzufügen und nicht zu bemerken, dass es nicht ausgeführt wird. Sie können genauso gut einen Kommentar nach dem Wert hinzufügen, den Sie zurückgeben möchten, zBdosomething() # this is my return value, don't add anything after it unless you know goddam well what you are doing
lebatsnok

10

Meine Frage ist: Warum nicht returnschneller anrufen

Es ist schneller, weil returnes eine (primitive) Funktion in R ist, was bedeutet, dass die Verwendung in Code die Kosten eines Funktionsaufrufs verursacht. Vergleichen Sie dies mit den meisten anderen Programmiersprachen, bei denen returnes sich um ein Schlüsselwort, aber nicht um einen Funktionsaufruf handelt: Es wird nicht in eine Laufzeitcode-Ausführung übersetzt.

Das heißt, das Aufrufen einer primitiven Funktion auf diese Weise ist in R ziemlich schnell, und das Aufrufen returnverursacht einen winzigen Overhead. Dies ist nicht das Argument für das Weglassen return.

oder besser und damit vorzuziehen?

Weil es keinen Grund gibt, es zu benutzen.

Weil es redundant ist und keine nützliche Redundanz hinzufügt .

Um es klar auszudrücken : Redundanz kann manchmal nützlich sein . Aber die meisten Redundanzen sind nicht von dieser Art. Stattdessen ist es von der Art, fügt visuelle Unordnung ohne Hinzufügen von Informationen: Es ist die Programmierung Äquivalent eines Füllwort oder Graphikmüll ).

Betrachten Sie das folgende Beispiel eines erklärenden Kommentars, der allgemein als schlechte Redundanz anerkannt wird, da der Kommentar lediglich umschreibt, was der Code bereits ausdrückt:

# Add one to the result
result = x + 1

Die Verwendung returnin R fällt in dieselbe Kategorie, da R eine funktionale Programmiersprache ist und in R jeder Funktionsaufruf einen Wert hat . Dies ist eine grundlegende Eigenschaft von R. Und sobald Sie R-Code aus der Perspektive sehen, dass jeder Ausdruck (einschließlich jedes Funktionsaufrufs) einen Wert hat, stellt sich die Frage: "Warum sollte ich verwenden return?" Es muss einen positiven Grund geben, da standardmäßig nicht verwendet wird.

Ein solcher positiver Grund besteht darin, ein vorzeitiges Verlassen einer Funktion zu signalisieren, beispielsweise in einer Schutzklausel :

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

Dies ist eine gültige, nicht redundante Verwendung von return. Solche Schutzklauseln sind jedoch in R im Vergleich zu anderen Sprachen selten, und da jeder Ausdruck einen Wert hat, iferfordert ein reguläres nicht return:

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

Wir können sogar so umschreiben f:

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

… Wo if (cond) exprist das gleiche wie if (cond) expr else NULL.

Abschließend möchte ich drei häufigen Einwänden vorbeugen:

  1. Einige Leute argumentieren, dass die Verwendung returnKlarheit schafft, weil sie signalisiert, dass „diese Funktion einen Wert zurückgibt“. Wie oben erläutert, gibt jede Funktion in R etwas zurück. returnAls Marker für die Rückgabe eines Werts zu betrachten, ist nicht nur redundant, sondern auch aktiv irreführend .

  2. In ähnlicher Weise hat das Zen von Python eine wunderbare Richtlinie, die immer befolgt werden sollte:

    Explizit ist besser als implizit.

    Wie verstößt das Löschen redundanter Daten returnnicht dagegen? Weil der Rückgabewert einer Funktion in einer funktionalen Sprache immer explizit ist: Es ist ihr letzter Ausdruck. Dies ist wieder das gleiche Argument über explizite Aussagen gegen Redundanz.

    Wenn Sie explizite Aussagen machen möchten, markieren Sie damit die Ausnahme von der Regel: Markieren Sie Funktionen, die keinen aussagekräftigen Wert zurückgeben und nur wegen ihrer Nebenwirkungen aufgerufen werden (z. B. cat). Außer R hat einen besseren Marker als returnfür diesen Fall : invisible. Zum Beispiel würde ich schreiben

    save_results = function (results, file) {
        # … code that writes the results to a file …
        invisible()
    }
  3. Aber was ist mit langen Funktionen? Wird es nicht leicht sein, den Überblick über die Rückgabe zu verlieren?

    Zwei Antworten: Erstens nicht wirklich. Die Regel ist klar: Der letzte Ausdruck einer Funktion ist ihr Wert. Es gibt nichts zu verfolgen.

    Noch wichtiger ist jedoch, dass das Problem bei langen Funktionen nicht das Fehlen expliziter returnMarker ist. Es ist die Länge der Funktion . Lange Funktionen verstoßen fast (?) Immer gegen das Prinzip der Einzelverantwortung, und selbst wenn dies nicht der Fall ist, profitieren sie davon, dass sie aus Gründen der Lesbarkeit getrennt werden.


Vielleicht sollte ich hinzufügen, dass einige Leute dafür plädieren return, es anderen Sprachen ähnlicher zu machen. Aber das ist ein schlechtes Argument: Andere funktionale Programmiersprachen verwenden diese ebenfalls nicht return. Es sind nur zwingende Sprachen, in denen nicht jeder Ausdruck einen Wert hat, die ihn verwenden.
Konrad Rudolph

Ich bin zu dieser Frage mit der Meinung gekommen, dass die Verwendung von returnUnterstützungen ausdrücklich besser ist, und habe Ihre Antwort mit voller Kritik gelesen. Ihre Antwort hat mich veranlasst, über diese Ansicht nachzudenken. Ich denke, dass die Notwendigkeit, returnexplizit (zumindest in meinem Fall) zu verwenden, mit der Notwendigkeit verbunden ist, meine Funktionen zu einem späteren Zeitpunkt besser überarbeiten zu können. Mit der Vorstellung, dass meine Funktionen einfach zu komplex sein könnten, kann ich jetzt sehen, dass ein Ziel zur Verbesserung meines Programmierstils darin besteht, die Codes so zu strukturieren, dass die Explizität ohne a erhalten bleibt return. Vielen Dank für diese Überlegungen und Einsichten!
Kasper Thystrup Karstensen vor

6

Ich halte das für returneinen Trick. In der Regel wird der Wert des letzten in einer Funktion ausgewerteten Ausdrucks zum Wert der Funktion - und dieses allgemeine Muster findet sich an vielen Stellen. Alle folgenden Punkte ergeben 3:

local({
1
2
3
})

eval(expression({
1
2
3
}))

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

Was returnnicht wirklich ist , ist einen Wert zurückzugeben (dies geschieht mit oder ohne ihn), sondern die Funktion auf unregelmäßige Weise "auszubrechen". In diesem Sinne ist es das nächste Äquivalent der GOTO-Anweisung in R (es gibt auch break und next). Ich benutze returnsehr selten und nie am Ende einer Funktion.

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

... dies kann so umgeschrieben werden, if(a) a else bdass es viel besser lesbar und weniger geschweift ist. Hier ist das überhaupt nicht nötig return. Mein prototypischer Fall der Verwendung von "Rückkehr" wäre so etwas wie ...

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

Im Allgemeinen deutet die Notwendigkeit vieler Rückgaben darauf hin, dass das Problem entweder hässlich oder schlecht strukturiert ist

<>

return benötigt keine Funktion, um zu funktionieren: Sie können sie verwenden, um aus einer Reihe von Ausdrücken auszubrechen, die ausgewertet werden sollen.

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)

Heute habe ich einen Fall gefunden, in dem man tatsächlich etwas braucht return(mein hässliches Beispiel oben ist sehr künstlich): Angenommen, Sie müssen testen, ob ein Wert ist, NULLoder NA: Geben Sie in diesen Fällen eine leere Zeichenfolge zurück, andernfalls geben Sie den characterWert zurück. Aber ein Test von is.na(NULL)gibt einen Fehler, so dass es so aussieht, als ob er nur mit if(is.null(x)) return("")und dann fortgesetzt werden kann if(is.na(x)) ...... (Man kann length(x)==0anstelle von verwenden, is.null(x)aber es ist nicht möglich, length(x)==0 | is.na(x)wenn es xist NULL.)
lebatsnok

1
Das liegt daran, dass Sie |(vektorisiertes ODER, bei dem beide Seiten ausgewertet werden) anstelle von ||(Kurzschluss-ODER, nicht vektorisiert, bei dem Prädikate der Reihe nach ausgewertet werden) verwendet haben. Betrachten Sie if (TRUE | stop()) print(1)versusif (TRUE || stop()) print(1)
asac

2

return kann die Lesbarkeit des Codes verbessern:

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

3
Vielleicht kann es das auch. In Ihrem Beispiel ist dies jedoch nicht der Fall. Stattdessen wird der Code-Fluss verdeckt (oder vielmehr komplexisiert).
Konrad Rudolph

1
Ihre Funktion kann vereinfacht werden zu: foo <- function() a || b(was IMO besser lesbar ist; auf jeden Fall gibt es keine "reine" Lesbarkeit, aber Lesbarkeit nach Meinung von jemandem: Es gibt Leute, die sagen, Assemblersprache ist perfekt lesbar)
lebatsnok

1

Das Argument der Redundanz ist hier viel aufgetaucht. Meiner Meinung nach ist das nicht Grund genug, es wegzulassen return(). Redundanz ist nicht automatisch eine schlechte Sache. Bei strategischer Verwendung macht Redundanz den Code klarer und wartbarer.

Betrachten Sie dieses Beispiel: Funktionsparameter haben häufig Standardwerte. Die Angabe eines Werts, der dem Standardwert entspricht, ist daher redundant. Außer es macht das Verhalten deutlich, das ich erwarte. Sie müssen die Funktionsmanpage nicht aufrufen, um mich an die Standardeinstellungen zu erinnern. Und keine Sorge, dass eine zukünftige Version der Funktion ihre Standardeinstellungen ändert.

Mit einer vernachlässigbaren Leistungsstrafe für Anrufe return()(gemäß den hier von anderen veröffentlichten Benchmarks) kommt es eher auf den Stil als auf richtig und falsch an. Damit etwas "falsch" ist, muss es einen klaren Nachteil geben, und niemand hier hat zufriedenstellend gezeigt, dass das Einbeziehen oder Weglassen return()einen konsequenten Nachteil hat. Es scheint sehr fallspezifisch und benutzerspezifisch zu sein.

Hier stehe ich also dazu.

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

Ich fühle mich mit "verwaisten" Variablen wie im obigen Beispiel unwohl. Wurde abcdgehen Teil einer Anweisung, die ich nicht zu schreiben habe fertig? Ist es ein Rest eines Spleißes / einer Bearbeitung in meinem Code und muss gelöscht werden? Habe ich versehentlich etwas von einem anderen Ort eingefügt / verschoben?

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

Im Gegensatz dazu macht mir dieses zweite Beispiel klar, dass es sich eher um einen beabsichtigten Rückgabewert als um einen Unfall oder unvollständigen Code handelt. Für mich ist diese Redundanz absolut nicht nutzlos.

Sobald die Funktion beendet ist und funktioniert, kann ich natürlich die Rückgabe entfernen. Aber es zu entfernen ist an sich ein überflüssiger zusätzlicher Schritt und meiner Ansicht nach nutzloser, als es überhaupt einzubeziehen return().

Alles in allem verwende ich keine return()kurzen unbenannten Einzeilerfunktionen. Dort macht es einen großen Teil des Funktionscodes aus und verursacht daher meistens visuelle Unordnung, die den Code weniger lesbar macht. Aber für größere formal definierte und benannte Funktionen verwende ich es und werde es wahrscheinlich auch weiterhin tun.


"War abcd Teil einer Aussage, die ich nicht zu Ende geschrieben habe?" - Wie unterscheidet sich das von jedem anderen Ausdruck, den Sie schreiben? Dies ist meiner Meinung nach der Kern unserer Meinungsverschiedenheit. Ein variabler Stand für sich zu haben, mag in einer imperativen Programmiersprache eigen sein, ist aber völlig normal und wird in einer funktionalen Programmiersprache erwartet. Ich behaupte, das Problem ist einfach, dass Sie mit der funktionalen Programmierung nicht vertraut sind (die Tatsache, dass Sie von „Anweisungen“ anstelle von „Ausdrücken“ sprechen, verstärkt dies).
Konrad Rudolph

Es ist anders, weil jede andere Anweisung normalerweise etwas offensichtlicheres tut: Es ist eine Aufgabe, ein Vergleich, ein Funktionsaufruf ... Ja, meine ersten Codierungsschritte waren in imperativen Sprachen und ich verwende immer noch imperative Sprachen. Über alle Sprachen hinweg einheitliche visuelle Hinweise zu haben (wo immer die Sprachen dies zulassen), erleichtert mir die Arbeit. A return()in R kostet nichts. Es ist objektiv überflüssig, aber "nutzlos" zu sein, ist Ihr subjektives Urteil. Redundant und nutzlos sind nicht unbedingt Synonyme. Hier sind wir uns nicht einig.
Cymon

Außerdem bin ich weder Softwareentwickler noch Informatiker. Lesen Sie nicht zu viel Nuance in meine Terminologie.
Cymon

Nur um zu verdeutlichen: „Redundant und nutzlos sind nicht unbedingt Synonyme. Hier sind wir uns nicht einig. “ - Nein, dem stimme ich voll und ganz zu, und ich habe dies in meiner Antwort ausdrücklich erwähnt. Redundanz kann hilfreich oder sogar entscheidend sein . Dies muss jedoch aktiv gezeigt und nicht vorausgesetzt werden. Ich verstehe Ihr Argument dafür, warum Sie denken, dass dies zutrifft return, und obwohl ich nicht überzeugt bin, denke ich, dass es möglicherweise gültig ist (es ist definitiv in einer zwingenden Sprache… ich glaube, dass es nicht in funktionale Sprachen übersetzt wird).
Konrad Rudolph
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.