Umgang mit nicht bekannten Parameternamen einer Funktion beim Aufruf

13

Hier ist ein Programmier- / Sprachproblem, über das ich gerne Ihre Gedanken hören würde.

Wir haben Konventionen entwickelt, die die meisten Programmierer befolgen sollten, die nicht Teil der Sprachsyntax sind, aber dazu dienen, den Code besser lesbar zu machen. Diese sind natürlich immer umstritten, aber es gibt zumindest einige Kernkonzepte, die den meisten Programmierern zusagen. Benennen Sie Ihre Variablen entsprechend, benennen Sie sie im Allgemeinen, machen Sie Ihre Zeilen nicht zu lang, vermeiden Sie lange Funktionen, Kapseln und dergleichen.

Es gibt jedoch ein Problem, zu dem ich noch keinen Kommentar gefunden habe, und das könnte das größte des ganzen Haufens sein. Es ist das Problem, dass Argumente beim Aufrufen einer Funktion anonym bleiben.

Funktionen stammen aus der Mathematik, in der f (x) eine eindeutige Bedeutung hat, weil eine Funktion eine viel strengere Definition hat als normalerweise beim Programmieren. Reine Funktionen in der Mathematik können viel weniger als sie in der Programmierung können und sie sind ein viel eleganteres Werkzeug, sie nehmen normalerweise nur ein Argument (normalerweise eine Zahl) und sie geben immer einen Wert (normalerweise auch eine Zahl) zurück. Wenn eine Funktion mehrere Argumente akzeptiert, handelt es sich fast immer nur um zusätzliche Dimensionen der Funktionsdomäne. Mit anderen Worten, ein Argument ist nicht wichtiger als die anderen. Sicher, sie sind explizit geordnet, aber ansonsten haben sie keine semantische Ordnung.

Bei der Programmierung haben wir jedoch mehr Freiheitsgrade bei der Definition von Funktionen, und in diesem Fall würde ich behaupten, dass dies keine gute Sache ist. In einer normalen Situation haben Sie eine Funktion wie diese definiert

func DrawRectangleClipped (rectToDraw, fillColor, clippingRect) {}

Betrachtet man die Definition, so ist bei korrekter Schreibweise der Funktion klar, was was ist. Wenn Sie die Funktion aufrufen, kann es sein, dass in Ihrer IDE / Ihrem Editor Intellisense- / Code-Vervollständigungsmagie vor sich geht, die Ihnen sagt, was das nächste Argument sein soll. Aber warte. Wenn ich das brauche, während ich den Anruf schreibe, fehlt hier nicht etwas? Die Person, die den Code liest, hat nicht den Vorteil einer IDE, und wenn sie nicht zur Definition springt, weiß sie nicht, welches der beiden als Argumente übergebenen Rechtecke wofür verwendet wird.

Das Problem geht noch weiter. Wenn unsere Argumente von einer lokalen Variablen stammen, kann es vorkommen, dass wir nicht einmal wissen, was das zweite Argument ist, da wir nur den Variablennamen sehen. Nehmen Sie zum Beispiel diese Codezeile

DrawRectangleClipped(deserializedArray[0], deserializedArray[1], deserializedArray[2])

Dies wird in unterschiedlichem Maße in verschiedenen Sprachen, aber auch in streng typisierten Sprachen, vermieden. Selbst wenn Sie Ihre Variablen vernünftig benennen, erwähnen Sie nicht einmal den Typ der Variablen, wenn Sie sie an die Funktion übergeben.

Wie bei der Programmierung üblich, gibt es viele mögliche Lösungen für dieses Problem. Viele sind bereits in gängigen Sprachen implementiert. Zum Beispiel benannte Parameter in C #. Alles, was ich weiß, hat jedoch erhebliche Nachteile. Die Benennung jedes Parameters bei jedem Funktionsaufruf kann möglicherweise nicht zu lesbarem Code führen. Es fühlt sich fast so an, als ob wir über die Möglichkeiten der Klartextprogrammierung hinauswachsen. Wir sind in fast allen Bereichen von NUR Text weggegangen, aber wir codieren immer noch das Gleiche. Weitere Informationen werden benötigt, um im Code angezeigt zu werden? Füge mehr Text hinzu. Wie auch immer, das wird ein bisschen tangential, also höre ich hier auf.

Eine Antwort, die ich auf das zweite Code-Snippet erhielt, war, dass Sie wahrscheinlich zuerst das Array in einige benannte Variablen entpacken und diese dann verwenden würden, aber der Variablenname kann viele Dinge bedeuten, und die Art und Weise, wie er aufgerufen wird, sagt Ihnen nicht unbedingt, wie er sein soll im Kontext der aufgerufenen Funktion interpretiert werden. Im lokalen Bereich haben Sie möglicherweise zwei Rechtecke mit den Namen leftRectangle und rightRectangle, da diese semantisch dargestellt werden, sie müssen jedoch nicht auf das erweitert werden, was sie darstellen, wenn sie einer Funktion zugewiesen werden.

Wenn Ihre Variablen im Kontext der aufgerufenen Funktion benannt werden, geben Sie weniger Informationen ein, als Sie mit diesem Funktionsaufruf möglicherweise könnten, und auf einer bestimmten Ebene führt dies zu einem schlechteren Code. Wenn Sie eine Prozedur haben, die ein Rechteck ergibt, das Sie in rectForClipping speichern, und dann eine andere Prozedur, die rectForDrawing bereitstellt, dann ist der eigentliche Aufruf von DrawRectangleClipped nur eine Zeremonie. Eine Zeile, die nichts Neues bedeutet und nur vorhanden ist, damit der Computer genau weiß, was Sie möchten, obwohl Sie dies bereits mit Ihrer Benennung erklärt haben. Das ist keine gute Sache.

Ich würde wirklich gerne neue Perspektiven dazu hören. Ich bin mir sicher, dass ich nicht der erste bin, der dies als Problem ansieht. Wie wird es also gelöst?

Darwin
quelle
2
Ich bin verwirrt, was das genaue Problem ist ... hier scheinen verschiedene Ideen zu sein, nicht sicher, welche Ihre Hauptidee ist.
FrustratedWithFormsDesigner
1
Die Dokumentation der Funktion sollte Ihnen sagen, was die Argumente bewirken. Sie könnten einwenden, dass jemand, der den Code liest, möglicherweise nicht über die Dokumentation verfügt, aber dann nicht wirklich weiß, was der Code tut und welche Bedeutung er aus dem Lesen zieht, ist eine fundierte Vermutung. In jedem Kontext, in dem der Leser wissen muss, dass der Code korrekt ist, wird er die Dokumentation benötigen.
Doval
3
@Darwin In der funktionalen Programmierung haben alle Funktionen immer noch nur 1 Argument. Wenn Sie "mehrere Argumente" übergeben müssen, ist der Parameter normalerweise ein Tupel (wenn Sie möchten, dass sie sortiert werden) oder ein Datensatz (wenn Sie nicht möchten, dass sie sortiert werden). Darüber hinaus ist es trivial, jederzeit spezialisierte Versionen von Funktionen zu erstellen, sodass Sie die Anzahl der erforderlichen Argumente reduzieren können. Da so gut wie jede funktionale Sprache Syntax für Tupel und Datensätze bereitstellt, ist das Bündeln von Werten schmerzlos und Sie erhalten kostenlos Komposition (Sie können Funktionen, die Tupel zurückgeben, mit denen, die Tupel nehmen, verketten.)
Doval
1
@Bergi Die Leute verallgemeinern in reinen FPs viel mehr, daher denke ich, dass die Funktionen selbst normalerweise kleiner und zahlreicher sind. Ich könnte aber weit weg sein. Ich habe nicht viel Erfahrung darin, mit Haskell und der Gang an echten Projekten zu arbeiten.
Darwin
4
Ich denke, die Antwort lautet "Benennen Sie Ihre Variablen nicht 'deserializedArray'"?
Whatsisname

Antworten:

10

Ich bin damit einverstanden, dass die Art und Weise, wie Funktionen häufig verwendet werden, verwirrend für das Schreiben von Code und insbesondere das Lesen von Code sein kann.

Die Antwort auf dieses Problem hängt zum Teil von der Sprache ab. Wie Sie bereits erwähnt haben, hat C # Parameter benannt. Die Lösung von Objective-C für dieses Problem umfasst aussagekräftigere Methodennamen. Beispielsweise stringByReplacingOccurrencesOfString:withString:handelt es sich um eine Methode mit eindeutigen Parametern.

In Groovy verwenden einige Funktionen Maps, die eine Syntax wie die folgende zulassen:

restClient.post(path: 'path/to/somewhere',
            body: requestBody,
            requestContentType: 'application/json')

Im Allgemeinen können Sie dieses Problem beheben, indem Sie die Anzahl der Parameter begrenzen, die Sie an eine Funktion übergeben. Ich denke 2-3 ist eine gute Grenze. Wenn sich herausstellt, dass eine Funktion mehr Parameter benötigt, muss ich das Design überdenken. Dies kann jedoch allgemein schwieriger zu beantworten sein. Manchmal versuchen Sie, in einer Funktion zu viel zu tun. Manchmal ist es sinnvoll, eine Klasse zum Speichern Ihrer Parameter in Betracht zu ziehen. In der Praxis stelle ich häufig fest, dass Funktionen, die eine große Anzahl von Parametern annehmen, normalerweise viele davon als optional haben.

Selbst in einer Sprache wie Objective-C ist es sinnvoll, die Anzahl der Parameter zu begrenzen. Ein Grund ist, dass viele Parameter optional sind. Ein Beispiel finden Sie unter rangeOfString: und seinen Variationen in NSString .

Ein in Java häufig verwendetes Muster ist die Verwendung einer Klasse im flüssigen Stil als Parameter. Beispielsweise:

something.draw(new Box().withHeight(5).withWidth(20))

Dabei wird eine Klasse als Parameter verwendet, und mit einer Klasse im flüssigen Stil wird leicht lesbarer Code erstellt.

Das obige Java-Snippet hilft auch, wenn die Reihenfolge der Parameter nicht so offensichtlich ist. Wir gehen normalerweise bei Koordinaten davon aus, dass X vor Y steht. Und ich sehe normalerweise Höhe vor Breite als Konvention, aber das ist immer noch nicht sehr klar ( something.draw(5, 20)).

Ich habe auch einige Funktionen wie gesehen, drawWithHeightAndWidth(5, 20)aber selbst diese können nicht zu viele Parameter annehmen, oder Sie würden anfangen, die Lesbarkeit zu verlieren.

David V
quelle
2
Die Reihenfolge kann in der Tat sehr schwierig sein, wenn Sie mit dem Java-Beispiel fortfahren. Vergleichen Sie zum Beispiel die folgenden Konstruktoren von awt: Dimension(int width, int height)und GridLayout(int rows, int cols)(die Anzahl der Zeilen ist die Höhe, dh GridLayoutdie Höhe zuerst und Dimensiondie Breite).
Pierre Arlaud
1
Solche Inkonsistenzen wurden auch bei PHP ( eev.ee/blog/2012/04/09/php-a-fractal-of-bad-design ) stark kritisiert , zum Beispiel: array_filter($input, $callback)versus array_map($callback, $input), strpos($haystack, $needle)versusarray_search($needle, $haystack)
Pierre Arlaud
12

Meistens wird dies durch eine gute Benennung von Funktionen, Parametern und Argumenten gelöst. Sie haben dies jedoch bereits untersucht und festgestellt, dass es Mängel aufweist. Die meisten dieser Mängel werden dadurch behoben, dass Funktionen mit einer geringen Anzahl von Parametern sowohl im aufrufenden als auch im aufgerufenen Kontext klein gehalten werden. Ihr spezielles Beispiel ist problematisch, da die aufgerufene Funktion versucht, mehrere Aufgaben gleichzeitig auszuführen: Festlegen eines Basisrechtecks, Festlegen eines Beschneidungsbereichs, Zeichnen und Füllen mit einer bestimmten Farbe.

Das ist so, als würde man versuchen, einen Satz nur mit den Adjektiven zu schreiben. Fügen Sie dort weitere Verben (Funktionsaufrufe) ein, erstellen Sie ein Subjekt (Objekt) für Ihren Satz und es ist einfacher zu lesen:

rect.clip(clipRect).fill(color)

Auch wenn clipRectund colorhaben schreckliche Namen (und sie sollten nicht), können Sie immer noch ihre Typen aus dem Kontext unterscheiden.

Ihr deserialisiertes Beispiel ist problematisch, weil der aufrufende Kontext versucht, zu viel auf einmal zu tun: etwas zu deserialisieren und zu zeichnen. Sie müssen sinnvolle Namen vergeben und die beiden Verantwortlichkeiten klar voneinander trennen. Zumindest so etwas:

(rect, clipRect, color) = deserializeClippedRect()
rect.clip(clipRect).fill(color)

Viele Lesbarkeitsprobleme werden dadurch verursacht, dass versucht wird, zu präzise zu sein und Zwischenschritte übersprungen werden, die Menschen benötigen, um Kontext und Semantik zu erkennen.

Karl Bielefeldt
quelle
1
Ich mag die Idee, mehrere Funktionsaufrufe hintereinander anzuordnen, um die Bedeutung zu verdeutlichen, aber geht das nicht nur um das Problem herum? Es ist im Grunde "Ich möchte einen Satz schreiben, aber die Sprache, die ich verklage, lässt mich nicht, so dass ich nur das nächste Äquivalent verwenden kann"
Darwin
@ Darwin IMHO, es ist nicht so, dass dies durch eine natürlichere Programmiersprache verbessert werden könnte. Natürliche Sprachen sind sehr vieldeutig und wir können sie nur im Kontext verstehen und können tatsächlich nie sicher sein. Das Aufrufen von Funktionen ist viel besser, da jeder Begriff (idealerweise) über Dokumentation und verfügbare Quellen verfügt und die Struktur durch Klammern und Punkte verdeutlicht wird.
Maaartinus
3

In der Praxis wird dies durch ein besseres Design gelöst. Es ist ausnahmsweise ungewöhnlich, dass gut geschriebene Funktionen mehr als zwei Eingaben annehmen, und wenn dies geschieht, ist es ungewöhnlich, dass diese vielen Eingaben nicht zu einem zusammenhängenden Bündel zusammengefasst werden können. Dies macht es ziemlich einfach, Funktionen oder Aggregatparameter aufzubrechen, damit eine Funktion nicht zu viel bewirkt. Wenn es zwei Eingänge hat, wird es einfach zu benennen und es wird viel klarer, welcher Eingang welcher ist.

Meine Spielzeugsprache hatte das Konzept von Phrasen, um damit umzugehen, und andere, auf natürliche Sprachen fokussierte Programmiersprachen hatten andere Ansätze, um damit umzugehen, aber sie haben alle andere Nachteile. Außerdem sind gerade Phrasen nicht viel mehr als eine nette Syntax dafür, dass Funktionen bessere Namen haben. Es wird immer schwierig sein, einen guten Funktionsnamen zu erstellen, wenn eine Reihe von Eingaben erforderlich sind.

Telastyn
quelle
Sätze scheinen wirklich ein Schritt vorwärts zu sein. Ich weiß, dass einige Sprachen ähnliche Fähigkeiten haben, aber es ist bei weitem nicht verbreitet. Ganz zu schweigen von all dem Makrohass, der von C (++) Puristen kommt, die nie richtig gemachte Makros verwendet haben. Möglicherweise werden wir nie Funktionen wie diese in populären Sprachen haben.
Darwin
Willkommen zum allgemeinen Thema domänenspezifischer Sprachen . Ich würde mir wirklich wünschen, dass mehr Menschen den Vorteil von ... (+1)
Izkata,
2

In Javascript (oder ECMAScript ), zum Beispiel, wuchsen viele Programmierer daran gewöhnt,

Übergeben von Parametern als Satz benannter Objekteigenschaften in einem einzelnen anonymen Objekt.

Und als Programmierpraxis gelangte es von Programmierern zu ihren Bibliotheken und von dort zu anderen Programmierern, die es immer mehr mochten und benutzten und weitere Bibliotheken usw. schrieben.

Beispiel

Anstatt anzurufen

function drawRectangleClipped (rectToDraw, fillColor, clippingRect)

so was:

drawRectangleClipped(deserializedArray[0], deserializedArray[1], deserializedArray[2])

Dies ist ein gültiger und korrekter Stil, den Sie als

function drawRectangleClipped (params)

so was:

drawRectangleClipped({
    rectToDraw: deserializedArray[0], 
    fillColor: deserializedArray[1], 
    clippingRect: deserializedArray[2]
})

, das ist gültig und richtig und nett in Bezug auf Ihre Frage.

Natürlich muss es dafür geeignete Bedingungen geben - in JavaScript ist dies weitaus praktikabler als beispielsweise in C. In JavaScript entstand sogar die mittlerweile weit verbreitete strukturelle Notation, die sich als leichteres Gegenstück zu XML großer Beliebtheit erfreute. Es heißt JSON (Sie haben vielleicht schon davon gehört).

Pavel
quelle
Ich weiß nicht genug über diese Sprache, um die Syntax zu überprüfen, aber insgesamt mag ich diesen Beitrag. Scheint ziemlich elegant. +1
IT Alex
Sehr oft wird dies mit normalen Argumenten kombiniert, dh es folgen 1-3 Argumente params(oft mit optionalen Argumenten und oft selbst optional), wie z . B. diese Funktion . Dies macht Funktionen mit vielen Argumenten ziemlich einfach zu verstehen (in meinem Beispiel gibt es 2 obligatorische und 6 optionale Argumente).
Maaartinus
0

Sie sollten dann Objective-C verwenden, hier ist eine Funktionsdefinition:

- (id)performSelector:(SEL)aSelector withObject:(id)anObject withObject:(id)anotherObject

Und hier wird es verwendet:

[someObject performSelector:someSelector withObject:someObject2 withObject:someObject3];

Ich denke Ruby hat ähnliche Konstrukte und man kann sie in anderen Sprachen mit Schlüsselwertlisten simulieren.

Für komplexe Funktionen in Java definiere ich gerne Dummy-Variablen im Funktionswortlaut. Für Ihr Links-Rechts-Beispiel:

Rectangle referenceRectangle = leftRectangle;
Rectangle targetRectangle = rightRectangle;
doSomeWeirdStuffWithRectangles(referenceRectangle, targetRectangle);

Sieht nach mehr Code aus, aber Sie können zum Beispiel leftRectangle verwenden und den Code später mit "Lokale Variable extrahieren" umgestalten, wenn Sie der Meinung sind, dass dies für einen zukünftigen Betreuer des Codes, der Sie sein könnte oder nicht, nicht verständlich ist.

awsm
quelle
Über dieses Java-Beispiel schrieb ich in der Frage, warum ich denke, dass es keine gute Lösung ist. Was denkst du darüber?
Darwin
0

Mein Ansatz ist temporäre lokale Variablen zu schaffen - aber nur um sie nicht nennen LeftRectangeund RightRectangle. Ich benutze eher etwas längere Namen, um mehr Bedeutung zu vermitteln. Ich versuche oft, die Namen so weit wie möglich zu unterscheiden, zB nicht beide zu nennen something_rectangle, wenn ihre Rolle nicht sehr symmetrisch ist.

Beispiel (C ++):

auto& connector_source = deserializedArray[0]; 
auto& connector_target = deserializedArray[1]; 
auto& bounding_box = deserializedArray[2]; 
DoWeirdThing(connector_source, connector_target, bounding_box)

und ich könnte sogar eine einzeilige Wrapper-Funktion oder -Vorlage schreiben:

template <typename T1, typename T2, typename T3>
draw_bounded_connector(
    T1& connector_source, T2& connector_target,const T3& bounding_box) 
{
    DoWeirdThing(connector_source, connector_target, bounding_box)
}

(Ignorieren Sie das kaufmännische Und, wenn Sie C ++ nicht kennen).

Wenn die Funktion mehrere seltsame Dinge ohne gute Beschreibung ausführt, muss sie wahrscheinlich überarbeitet werden!

einpoklum
quelle