Wie kann ich funktionalen JavaScript-Code lesen?

9

Ich glaube, ich habe einige / viele / die meisten Grundkonzepte gelernt, die der funktionalen Programmierung in JavaScript zugrunde liegen. Ich habe jedoch Probleme beim spezifischen Lesen von Funktionscode, selbst von Code, den ich geschrieben habe, und frage mich, ob mir jemand Hinweise, Tipps, Best Practices, Terminologie usw. geben kann, die helfen können.

Nehmen Sie den folgenden Code. Ich habe diesen Code geschrieben. Ziel ist es, eine prozentuale Ähnlichkeit zwischen zwei Objekten zwischen say {a:1, b:2, c:3, d:3}und zuzuweisen {a:1, b:1, e:2, f:2, g:3, h:5}. Ich habe den Code als Antwort auf diese Frage zu Stack Overflow erstellt . Da ich nicht genau wusste, nach welcher prozentualen Ähnlichkeit das Poster fragte, stellte ich vier verschiedene Arten zur Verfügung:

  • der Prozentsatz der Schlüssel im 1. Objekt, der im 2. gefunden werden kann,
  • der Prozentsatz der Werte im 1. Objekt, der im 2. gefunden werden kann, einschließlich Duplikate,
  • der Prozentsatz der Werte im 1. Objekt, der im 2. Objekt gefunden werden kann, ohne dass Duplikate zulässig sind, und
  • Der Prozentsatz der {key: value} -Paare im 1. Objekt, der im 2. Objekt gefunden wird.

Ich begann mit einigermaßen zwingendem Code, erkannte jedoch schnell, dass dies ein Problem war, das für die funktionale Programmierung gut geeignet war. Insbesondere wurde mir klar, dass ich es sein könnte, wenn ich eine oder drei Funktionen für jede der oben genannten vier Strategien extrahieren könnte, die den Typ des Features definieren, das ich vergleichen wollte (z. B. die Schlüssel oder die Werte usw.) in der Lage, den Rest des Codes in wiederholbare Einheiten zu reduzieren (verzeihen Sie das Wortspiel). Sie wissen, es trocken zu halten. Also wechselte ich zur funktionalen Programmierung. Ich bin ziemlich stolz auf das Ergebnis, finde es einigermaßen elegant und verstehe, was ich ganz gut gemacht habe.

Selbst wenn ich den Code selbst geschrieben habe und jeden Teil davon während der Erstellung verstanden habe, bin ich, wenn ich jetzt darauf zurückblicke, mehr als ein wenig verblüfft darüber, wie man eine bestimmte halbe Zeile liest und wie man sie liest "grok", was eine bestimmte halbe Codezeile tatsächlich tut. Ich mache mentale Pfeile, um verschiedene Teile zu verbinden, die sich schnell in ein Durcheinander von Spaghetti verwandeln.

Kann mir jemand sagen, wie ich einige der komplizierteren Codebits auf eine Weise "lesen" kann, die sowohl präzise ist als auch zu meinem Verständnis dessen beiträgt, was ich lese? Ich denke, die Teile, die mich am meisten erreichen, sind diejenigen, die mehrere fette Pfeile hintereinander haben und / oder Teile, die mehrere Klammern hintereinander haben. Auch hier kann ich im Kern die Logik herausfinden, aber (ich hoffe) es gibt einen besseren Weg, um schnell und klar und direkt eine Reihe funktionaler JavaScript-Programmierung "aufzunehmen".

Sie können auch eine beliebige Codezeile von unten oder andere Beispiele verwenden. Wenn Sie jedoch erste Vorschläge von mir wünschen, sind hier einige. Beginnen Sie mit einem relativ einfachen. Gegen Ende des Codes wird dieser als Parameter an eine Funktion übergeben : obj => key => obj[key]. Wie liest und versteht man das? Ein längeres Beispiel ist eine Vollfunktion von Anfang an : const getXs = (obj, getX) => Object.keys(obj).map(key => getX(obj)(key));. Der letzte mapTeil bringt mich besonders.

Bitte beachten Sie , an diesem Punkt in der Zeit ist ich nicht die Suche nach Hinweisen auf Haskell oder symbolische abstrakte Notation oder die Grundlagen des currying usw. Was ich bin auf der Suche nach ist englische Sätze , dass ich still Mund kann , während bei einer Codezeile suchen. Wenn Sie Referenzen haben, die genau das ansprechen, großartig, aber ich suche auch nicht nach Antworten, die besagen, dass ich einige grundlegende Lehrbücher lesen sollte. Ich habe das getan und bekomme (zumindest einen erheblichen Teil) der Logik. Beachten Sie auch, dass ich keine erschöpfenden Antworten benötige (obwohl solche Versuche willkommen wären): Selbst kurze Antworten, die eine elegante Möglichkeit bieten, eine bestimmte Zeile ansonsten problematischen Codes zu lesen, wären willkommen.

Ich nehme an, ein Teil dieser Frage lautet: Kann ich Funktionscode sogar linear von links nach rechts und von oben nach unten lesen? Oder ist man ziemlich gezwungen, ein mentales Bild von spaghettiartigen Verkabelungen auf der Codeseite zu erstellen, das entschieden nicht linear ist? Und wenn man das tun muss , müssen wir den Code noch lesen. Wie nehmen wir also linearen Text und verdrahten die Spaghetti?

Alle Tipps wäre dankbar.

const obj1 = { a:1, b:2, c:3, d:3 };
const obj2 = { a:1, b:1, e:2, f:2, g:3, h:5 };

// x or X is key or value or key/value pair

const getXs = (obj, getX) =>
  Object.keys(obj).map(key => getX(obj)(key));

const getPctSameXs = (getX, filter = vals => vals) =>
  (objA, objB) =>
    filter(getXs(objB, getX))
      .reduce(
        (numSame, x) =>
          getXs(objA, getX).indexOf(x) > -1 ? numSame + 1 : numSame,
        0
      ) / Object.keys(objA).length * 100;

const pctSameKeys       = getPctSameXs(obj => key => key);
const pctSameValsDups   = getPctSameXs(obj => key => obj[key]);
const pctSameValsNoDups = getPctSameXs(obj => key => obj[key], vals => [...new Set(vals)]);
const pctSameProps      = getPctSameXs(obj => key => JSON.stringify( {[key]: obj[key]} ));

console.log('obj1:', JSON.stringify(obj1));
console.log('obj2:', JSON.stringify(obj2));
console.log('% same keys:                   ', pctSameKeys      (obj1, obj2));
console.log('% same values, incl duplicates:', pctSameValsDups  (obj1, obj2));
console.log('% same values, no duplicates:  ', pctSameValsNoDups(obj1, obj2));
console.log('% same properties (k/v pairs): ', pctSameProps     (obj1, obj2));

// output:
// obj1: {"a":1,"b":2,"c":3,"d":3}
// obj2: {"a":1,"b":1,"e":2,"f":2,"g":3,"h":5}
// % same keys:                    50
// % same values, incl duplicates: 125
// % same values, no duplicates:   75
// % same properties (k/v pairs):  25
Andrew Willems
quelle

Antworten:

18

Sie haben meistens Schwierigkeiten, es zu lesen, weil dieses Beispiel nicht sehr gut lesbar ist. Keine Straftat beabsichtigt, ein entmutigend großer Anteil der Proben, die Sie im Internet finden, auch nicht. Viele Leute spielen nur an den Wochenenden mit funktionaler Programmierung herum und müssen sich nie wirklich mit der langfristigen Aufrechterhaltung des Produktionsfunktionscodes befassen. Ich würde es eher so schreiben:

function mapObj(obj, f) {
  return Object.keys(obj).map(key => f(obj, key));
}

function getPctSameXs(obj1, obj2, f) {
  const mapped1 = mapObj(obj1, f);
  const mapped2 = mapObj(obj2, f);
  const same = mapped1.filter(x => mapped2.indexOf(x) != -1);
  const percent = same.length / mapped1.length * 100;
  return percent;
}

const getValues = (obj, key) => obj[key];
const valuesWithDupsPercent = getPctSameXs(obj1, obj2, getValues);

Aus irgendeinem Grund haben viele Leute die Idee im Kopf, dass Funktionscode ein bestimmtes ästhetisches "Aussehen" eines großen verschachtelten Ausdrucks haben sollte. Beachten Sie, dass meine Version, obwohl sie mit allen Semikolons etwas dem imperativen Code ähnelt, unveränderlich ist, sodass Sie alle Variablen ersetzen und einen großen Ausdruck erhalten können, wenn Sie möchten. Es ist zwar genauso "funktional" wie die Spaghetti-Version, aber besser lesbar.

Hier werden die Ausdrücke in sehr kleine Stücke und Vornamen aufgeteilt, die für die Domäne von Bedeutung sind. Das Verschachteln wird vermieden, indem allgemeine Funktionen wie mapObjin eine benannte Funktion gezogen werden. Lambdas sind für sehr kurze Funktionen mit einem klaren Zweck im Kontext reserviert.

Wenn Sie auf schwer lesbaren Code stoßen, überarbeiten Sie ihn, bis er einfacher ist. Es braucht etwas Übung, aber es lohnt sich. Funktionscode kann genauso lesbar wie zwingend sein. In der Tat oft mehr, weil es in der Regel prägnanter ist.

Karl Bielefeldt
quelle
Auf keinen Fall beleidigt! Ich werde zwar immer noch behaupten, dass ich einige Dinge über funktionale Programmierung weiß , aber vielleicht waren meine Aussagen in der Frage, wie viel ich weiß, etwas übertrieben. Ich bin wirklich ein relativer Anfänger. Zu sehen, wie dieser spezielle Versuch von mir so präzise, ​​klar und dennoch funktional umgeschrieben werden kann, scheint Gold zu sein ... danke. Ich werde Ihr Umschreiben sorgfältig studieren.
Andrew Willems
1
Ich habe gehört, dass lange Ketten und / oder das Verschachteln von Methoden unnötige Zwischenvariablen eliminieren. Im Gegensatz dazu zerlegt Ihre Antwort meine Ketten / Verschachtelungen in eigenständige Zwischenanweisungen unter Verwendung gut benannter Zwischenvariablen. Ich finde Ihren Code in diesem Fall besser lesbar, aber ich frage mich, wie allgemein Sie versuchen zu sein. Wollen Sie damit sagen, dass lange Methodenketten und / oder tiefes Verschachteln oft oder sogar immer ein zu vermeidendes Anti-Muster sind, oder gibt es Zeiten, in denen sie einen erheblichen Nutzen bringen? Und ist die Antwort auf diese Frage für funktionale und imperative Codierung unterschiedlich?
Andrew Willems
3
Es gibt bestimmte Situationen, in denen das Eliminieren von Zwischenvariablen Klarheit schaffen kann. In FP möchten Sie beispielsweise fast nie einen Index in ein Array. Manchmal gibt es auch keinen guten Namen für das Zwischenergebnis. Nach meiner Erfahrung neigen die meisten Menschen jedoch dazu, zu weit in die andere Richtung zu irren.
Karl Bielefeldt
6

Ich habe nicht viel von hochfunktionalen Arbeit in Javascript gemacht (was ich sagen würde , das ist - die meisten Menschen über funktionale Javascript sprechen können unter Verwendung von Karten, Filter und reduziert, aber der Code seine eigenen geordneten Funktionen definiert , das ist etwas fortgeschrittener als das), aber ich habe es in Haskell getan, und ich denke, zumindest ein Teil der Erfahrung wird übersetzt. Ich gebe Ihnen ein paar Hinweise auf Dinge, die ich gelernt habe:

Die Angabe der Funktionstypen ist sehr wichtig. In Haskell müssen Sie nicht angeben, um welchen Funktionstyp es sich handelt. Die Aufnahme des Typs in die Definition erleichtert jedoch das Lesen erheblich. Während Javascript die explizite Eingabe nicht auf die gleiche Weise unterstützt, gibt es keinen Grund, die Typdefinition nicht in einen Kommentar aufzunehmen, z.

// getXs :: forall O, F . O -> (O -> String -> F) -> [F]
const getXs = (obj, getX) =>
    Object.keys(obj).map(key => getX(obj)(key));

Mit ein wenig Übung im Umgang mit solchen Typdefinitionen machen sie die Bedeutung einer Funktion viel klarer.

Das Benennen ist wichtig, vielleicht sogar noch wichtiger als bei der prozeduralen Programmierung. Viele funktionale Programme sind in einem sehr knappen Stil geschrieben, der stark von Konventionen geprägt ist (z. B. ist die Konvention, dass 'xs' eine Liste / ein Array und 'x' ein Element darin ist, sehr verbreitet), aber es sei denn, Sie verstehen diesen Stil leicht würde ich eine ausführlichere Benennung vorschlagen. Wenn Sie sich bestimmte Namen ansehen, die Sie verwendet haben, ist "getX" irgendwie undurchsichtig, und daher hilft "getXs" auch nicht wirklich viel. Ich würde "getXs" so etwas wie "applyToProperties" nennen, und "getX" wäre wahrscheinlich "propertyMapper". "getPctSameXs" wäre dann "ProzentPropertiesSameWith" ("mit").

Eine andere wichtige Sache ist das Schreiben von idiomatischem Code . Ich stelle fest, dass Sie eine Syntax verwenden a => b => some-expression-involving-a-and-b, um Curry-Funktionen zu erzeugen. Dies ist interessant und kann in einigen Situationen nützlich sein, aber Sie tun hier nichts, was von Curry-Funktionen profitiert, und es wäre idiomatischer, Javascript zu verwenden, um stattdessen traditionelle Funktionen mit mehreren Argumenten zu verwenden. Auf diese Weise können Sie auf einen Blick leichter erkennen, was los ist. Sie verwenden auch const name = lambda-expression, um Funktionen zu definieren, bei denen die Verwendung idiomatischer wäre function name (args) { ... }. Ich weiß, dass sie sich semantisch geringfügig unterscheiden, aber wenn Sie sich nicht auf diese Unterschiede verlassen, würde ich vorschlagen, wenn möglich die häufigere Variante zu verwenden.

Jules
quelle
5
+1 für Typen! Nur weil die Sprache sie nicht hat, heißt das nicht, dass Sie nicht über sie nachdenken müssen . Mehrere Dokumentationssysteme für ECMAScript verfügen über eine Typensprache zum Aufzeichnen der Funktionstypen. Einige ECMAScript-IDEs haben auch eine Typensprache (und normalerweise verstehen sie auch die Typensprachen für die wichtigsten Dokumentationssysteme), und sie können sogar rudimentäre Typprüfungen und heuristische Hinweise mithilfe dieser Typanmerkungen durchführen .
Jörg W Mittag
Sie haben mir viel zum Kauen gegeben: Typdefinitionen, aussagekräftige Namen, Redewendungen ... danke! Nur ein paar von vielen möglichen Kommentaren: Ich hatte nicht unbedingt vor, bestimmte Teile als Curry-Funktionen zu schreiben; Sie haben sich einfach so entwickelt, als ich meinen Code während des Schreibens überarbeitet habe. Ich kann jetzt sehen, dass dies nicht benötigt wurde, und selbst das Zusammenführen der Parameter dieser beiden Funktionen zu zwei Parametern für eine einzelne Funktion ist nicht nur sinnvoller, sondern macht das kurze Bit sofort zumindest zumindest lesbarer.
Andrew Willems
@ JörgWMittag, danke für deine Kommentare zur Wichtigkeit von Typen und für den Link zu dieser anderen Antwort, die du geschrieben hast. Ich benutze WebStorm und habe nicht bemerkt, dass WebStorm, je nachdem, wie ich Ihre andere Antwort gelesen habe, weiß, wie man jsdoc-ähnliche Anmerkungen interpretiert. Ich gehe von Ihrem Kommentar aus, dass jsdoc und WebStorm zusammen zum Kommentieren von funktionalem, nicht nur zwingendem Code verwendet werden können, aber ich müsste weiter vertiefen, um das wirklich zu wissen. Ich habe schon einmal mit jsdoc gespielt und jetzt, da ich weiß, dass WebStorm und ich dort zusammenarbeiten können, gehe ich davon aus, dass ich diese Funktion / diesen Ansatz mehr nutzen werde.
Andrew Willems
@Jules, nur um zu verdeutlichen, auf welche Curry-Funktion ich mich in meinem obigen Kommentar bezogen habe: Wie Sie angedeutet haben, kann jede Instanz von obj => key => ...vereinfacht werden, (obj, key) => ...da später getX(obj)(key)auch vereinfacht werden kann get(obj, key). Im Gegensatz dazu kann eine andere Curry-Funktion (getX, filter = vals => vals) => (objA, objB) => ...zumindest im Zusammenhang mit dem Rest des geschriebenen Codes nicht einfach vereinfacht werden.
Andrew Willems