Denken in Array.reduce() | Komentor

Frage

Ich möchte ein Array von Elementen filtern, indem ich die Funktion map() verwende.
Das Problem ist, dass herausgefilterte Elemente immer noch Platz im Array beanspruchen und ich sie vollständig löschen möchte.
Irgendeine Idee?

TL;DR

Wenn Sie map() und filter() in einer einzigen Schleife ausführen möchten, wäre der Weg, dies zu tun, Reduce(). Array.reduce() kann bei der ersten Begegnung etwas unintuitiv sein, daher ist hier mein bester Versuch einer sanften Einführung.

Reduzieren als Konzept

Wenn Sie wie ich sind, haben Sie vielleicht den Reduzieralgorithmus in Ihrem Kopf verwendet, um Ihnen bei der Auswahl zu helfen. Zu viele Restaurants zur Auswahl?

[“Lucky Robot”, “Ramen Tatsuya”, “Salt Lick”, “East Side Pies”]

Eine Möglichkeit, dieses Problem zu lösen, besteht darin, einfach zwei davon zu nehmen und zu entscheiden: Welchen von diesen beiden würdest du wählen?

„Glücksroboter“ > „Ramen Tatsuya“ ? „Glücklicher Roboter“: „Ramen Tatsuya“

Ok, heute hast du Lust auf Sushi, also gewinnt Lucky Robot Runde 1. Was nun?

„Glücksroboter“ > „Salzleckstein“ ?

BBQ klingt heute zu heavy.

„Lucky Robot“ > „East Side Pies“ ?

Oh verdammt. Weißt du, Pizza klingt gut.

Fertig. Wir haben unsere Auswahl auf eine reduziert. Jetzt haben wir einen einfachen Vergleich durchgeführt – was wäre, wenn wir basierend auf den Kosten vergleichen wollten oder basierend darauf, ob jemand in unserer Gruppe Veganer ist, oder basierend auf einer komplexen Multi-Faktor-Analyse?

Reduce gibt uns einen verallgemeinerten algorithmischen Rahmen, um Gruppenmitglieder zu vergleichen und als Gruppe auf sie einzuwirken, indem wir sie einzeln vergleichen. Wir vergleichen nicht jedes Mitglied mit jedem Mitglied – das wäre viel umständlicher – aber wir vergleichen jedes Mitglied per Proxy.

Dekonstruieren von Reduce ()

Genau wie Map-Filter usw. ist Reduce eine reine Funktion, die das Array, auf das sie wirkt, nicht ändert, sondern stattdessen ein neues Array zurückgibt.

Hier ist unsere Gesamtperspektive, um diese Anleitung zu leiten: Der Punkt von Reduce () ist, dass Sie ein Array von Werten in einen endgültigen Wert umwandeln. Dazu nehmen wir alle Elemente, werten sie alle aus und treffen einige programmatische Entscheidungen, die darauf basieren, wie sich diese einzelnen Werte auf unseren endgültigen Wert beziehen sollen.

Wie sieht das in der Praxis aus?

In seiner einfachsten und gebräuchlichsten Form nimmt Reduce ein Argument an:

Eine (typischerweise anonyme) Reducer-Funktion. (Wir werden dies von nun an als Reduzierer bezeichnen.)

Ein Aufruf zum Reduzieren sieht also normalerweise wie folgt aus:


let reducedOutput = sourceArray.reduce(reducer);

Wir müssen jetzt verstehen, wie man einen Reducer schreibt.

Denken Sie daran: Wie bei forEach map filter usw. rufen Sie nicht den Reducer auf – Reduce ruft ihn auf.

Wie bei der Funktion, die an diese Methoden übergeben wird, ist das erste, was Sie beachten sollten, dass Ihr Reducer jedes Element Ihres Quellarrays einzeln einspeist.

Jetzt können wir den letzten Codeblock so erweitern:

reducer = (____, oneElementOfSourceArray) => {
    // do something with those two things
};
reducedOutput = sourceArray.reduce(reducer);

Was ist das erste Argument? Ich bin froh, dass Sie gefragt haben – haben Sie Geduld, denn wir müssen hier eine zyklische Beziehung beschreiben, die im Kern dafür steht, wie Reduce funktioniert und warum es schwierig zu verstehen ist.

Was in diese Lücke kommt, ist der Rückgabewert des letzten Reducer-Aufrufs, der ein Ergebnis in Bearbeitung sein wird. Dies ist unser aktueller Entwurf unseres endgültigen Werts, auf den wir das Array „reduzieren“ – und wenn es das letzte Mal aufgerufen wird, mit dem endgültigen Wert des Arrays, dann ist es der endgültige Wert.

Aber wo bekommt man diesen Entwurf her? Sie müssen es zurückgeben. Der Rückgabewert des letzten Reducers wird dem nächsten Reducer als Argument zugeführt.

Jetzt wissen wir also genug, um so viel zu schreiben:

reducer = (lastResultInProgress, oneElementOfSourceArray) => {
    // do something with those two things
    return newResultInProgress;
};
reducedOutput = sourceArray.reduce(reducer);

Hier sehen wir also resultInProgress an zwei Stellen. Der Rückgabewert newResultInProgress wird von jedem Reducer-Aufruf als lastResultInProgress in den nächsten Reducer-Aufruf eingespeist.

Erstes vollständiges Beispiel

sourceArray = [1,2,3]; // sum will be 6.
reducer = (resultInProgress, oneElementOfSourceArray) => {
    resultInProgress = resultInProgress + oneElementOfSourceArray;
    return resultInProgress;
};
sum = sourceArray.reduce(reducer);
console.log(sum === 6); // true

Diese Version ist natürlich absichtlich pedantisch. Sobald Sie sich mit Reduce wohler fühlen, können Sie es so schreiben:

sum = [1,2,3].reduce((m, v) => m + v);

Im klassischen Anwendungsszenario von Reduce beginnt resultInProgress als der Wert bei Index 0 im Array, und das oneElementOfSourceArray im ersten Reducer-Aufruf ist der Wert bei Index 1 im Array.

Das bedeutet, dass der Reducer in unserem doppelt aufgerufen werden würde [1,2,3] Beispiel oben. Beim ersten Mal wären die Argumente Platzhalter = Reducer(sourceArray[0]QuellArray[1]), und beim zweiten Mal sehen wir das Äquivalent von placeholder = Reducer(placeholder, sourceArray[2]).

Wenn wir einen vierten Wert hätten, wäre der nächste Aufruf placeholder = Reducer(placeholder, sourceArray[3]).

Dann würden wir Platzhalter als unseren endgültigen reduzierten Wert zurückgeben.

Wenn Sie bis zu diesem Punkt gefolgt sind, dann verstehen Sie reduzieren. Herzliche Glückwünsche!

Was hier folgt, sind einige Details und Illustrationen, die uns dazu führen, wie wir Reduce anstelle von Map+Filter verwenden können, und uns dabei helfen, ein besseres Gefühl für Reduce insgesamt zu bekommen.

Erweiterte Anwendungen

Aussaat unserer Startwerte

In unseren bisherigen Reduce-Beispielen nimmt der Reducer beim ersten Aufruf den Wert in Index 0 als Startwert für resultInProgress. Wir können jedoch tatsächlich etwas anderes übergeben, um diesen Wert zu initialisieren, was dazu führt, dass die sourceArray-Elemente immer nur als zweites Argument an den Reducer übergeben werden. Schauen wir uns eine kleine Änderung an, die funktional identisch mit unserem vorherigen Beispiel ist, aber diese Seed-Funktionalität nutzt:

sourceArray = [1,2,3]; // sum will be 6.
seedResultInProgress = 0;
reducer = function(resultInProgress, oneElementOfSourceArray) {
    resultInProgress += oneElementOfSourceArray;
    return resultInProgress;
};
sum = sourceArray.reduce(reducer, seedResultInProgress);
console.log(sum === 6); // true

Und die Kurzfassung:

sum = [1,2,3].reduce((resultDraft, element) => resultDraft + element, 0);

Jetzt werden beim ersten Reducer-Aufruf 0 und 1 als Argumente übergeben (d. h. seedResultInProgress und sourceArray[0]) anstelle von 1 und 2 (dh sourceArray[0] QuellArray[1]).

Das Ergebnis ist in diesem Fall identisch; Reduce ist gut geschrieben, so dass, wenn unsere Ausgabe das gleiche Format wie die Elemente unseres Eingabearrays (Array von Zahlen -> Zahl) hat, kein Seed benötigt wird. Aber diese Fähigkeit zum Seeden öffnet die Tür zu einigen anderen Optionen, zu denen wir bald kommen werden.

Auf einen Wert filtern

Wir haben uns jetzt mit Reduce befasst, das aus alten Werten neue Werte macht – jetzt schauen wir uns Reduce an, das als Werkzeug fungiert, um ein Array auf einen seiner Werte herunterzufiltern. Anstatt auf eine Summe zu reduzieren, reduzieren wir ein Array von Zahlen auf das größte einzelne Element des Arrays.

[1,2,3,30,50,4,5,4].reduce((resultInProgress, elementOfSourceArray) => {
    if (elementOfSourceArray > resultInProgress) return elementOfSourceArray
    else return resultInProgress
}, 0);
// ^this would return the largest number in the list.

(Beachten Sie, dass wir den Reducer jetzt inline übergeben, was eher üblich ist, als ihn im Voraus zu deklarieren.)


[7,4,1,99,57,2,1,100].reduce((memo, val) => val > memo ? val : memo);

(Ein weiterer Lernmoment: Beachten Sie, dass wir das erste Argument im prägnanten Code memo nennen. Das ist der kanonische Begriff für dieses Argument, also verwenden wir diesen Begriff ab jetzt im prägnanten Code.)

Auf Teilmenge filtern

Nachdem wir das verstanden haben, schauen wir uns an, wie wir ein Array auf ein kleineres Array reduzieren können. Angenommen, wir möchten alle Werte herausfiltern, die kleiner als 3 sind.

Da unsere Ausgabe ein Array sein wird, aber die Elemente unseres Arrays Zahlen sind, müssen wir für dieses hier ein leeres Array initialisieren, um zu beginnen:

sourceArray = [1,2,3,4,5];
seed = [];
sourceArray.reduce((resultInProgress, elementOfSourceArray) => {
    if (elementOfSourceArray >= 3) {
        resultInProgress.push(elementOfSourceArray);
    }
    return resultInProgress; // <--always an array
}, seed);

(Tipp zum Verständnis der Kurzfassung: [1,2].concat(3) gibt zurück [1,2,3]eine praktische Funktion für die funktionale Programmierung.)

[1,2,3,4,5].reduce((memo, value) => value >= 3 ? memo.concat(value) : memo, []);

Beantwortung der Frage

Jetzt, da wir wissen, wie man auf diese Weise filtert, ist es ganz einfach zu sehen, wie wir die Funktionalität von map einbinden könnten – wir bearbeiten sie einfach, bevor wir sie in das Array resultInProgress/memo push()en.

In unserem Beispiel verwenden wir denselben Filter, und wenn er unsere Filterbedingung erfüllt, multiplizieren wir ihn mit zwei.

Diesmal nur die Kurzfassung:

reduced = [1,2,3,4,5].reduce((memo, value) => value >= 3 ? memo.concat(value * 2) : memo, []);
// reduced is [6,8,10]

Das war’s, jetzt wissen Sie, wie Sie mit Reduce in einem Rutsch filtern und mappen können. Wenn Sie sich mit Reduce so wohl fühlen, dass Sie das Gefühl haben, eine Lösung wie diese im Handumdrehen zu finden, bedeutet dies, dass Sie ein leistungsstarkes Werkzeug in Ihrem Arsenal haben und zu neuen Denkweisen über Probleme führen können.

Eine bessere Antwort?

Aber weißt du was? Wenn ich sowohl filtern als auch zuordnen wollte und es Code wäre, der mir wichtig wäre, würde ich es einfach so schreiben:

reduced = [1,2,3,4,5]
.filter(element => element >= 3)
.map(element => element * 2);

Hier sind meine Absichten viel offensichtlicher. Dieser Code kann auf einen Blick grokkiert werden, und die Methodennamen helfen mir, zu verstehen, was passieren soll.

Reduzieren ist großartig und ein sehr mächtiges Werkzeug für eine bestimmte Klasse von Problemen. Aber wenn Sie es außerhalb seines grundlegenden Anwendungsfalls verwenden, wird es weniger offensichtlich, was Sie tun. Die Lesbarkeit des Codes ist wichtiger, als alles in einer Schleife zu erledigen.

Was ist mit der Leistung?

Es ist auch algorithmisch äquivalent. Wir können ein Array von fünf Elementen einmal durchlaufen und zwei Operationen pro Element ausführen, oder wir können ein Array von fünf Elementen zweimal durchlaufen und eine Operation pro Element pro Schleife ausführen. Das sind nur 512 gegen 521.

Man könnte versucht sein zu glauben, dass der Overhead von 5 anonymen Funktionsaufrufen gegenüber 10 anonymen Funktionsaufrufen hier einen Unterschied machen würde. Wenn Javascript näher am Metall wäre, könnte das der Fall sein, aber man muss bedenken, dass JS-Code nur ein Vorschlag für den Interpreter ist. Tatsächlich können Funktionen wie filter() oft so optimiert werden, dass eine „manuelle“ Filterung innerhalb einer Reduce-Funktion nicht möglich ist.

Aus Neugier habe ich beschlossen, diese Hypothese in jsperf zu testen. Das Ergebnis?

Reduce-Map-Filter-jsperf.png

In diesem Fall ist es drei Größenordnungen schneller, die Logik nicht von Hand innerhalb von Reduce zu codieren. Dies verdeutlicht erneut das Prinzip, dass in höheren Sprachen wie JS das Auslagern von Logik in die integrierten Funktionen oft so ist, als würde man sich in die zugrunde liegende Low-Level-Sprache einklinken, in die Ihr Code kompiliert wird.

Auch hier sollten Sie sich, sofern Sie nicht auf massiven Arrays im Client arbeiten, mit der Lesbarkeit über der Geschwindigkeit befassen. Und wenn Du tun Arbeiten Sie zufällig mit massiven Arrays im Client? Überlegen Sie, ob es vorteilhafter wäre, diese Arbeit auf den Server auszulagern. Nein? Dann ist es an der Zeit, über Geschwindigkeit nachzudenken.

Konzentrieren Sie sich darauf, Code zu erstellen, der einfacher zu lesen und zu schreiben ist. Es ist einfach so, dass es normalerweise auch Ihren Code schneller macht.

Hausaufgaben

Wenn Sie durch Übung ein besseres Gefühl für Reduzieren bekommen möchten, hier sind einige Hausaufgaben:

Wie würden Sie einen Reducer schreiben, um ein Array von Arrays zu glätten?
Wie würden Sie Ihr eigenes Array.reduce schreiben?
Beispielantworten sind unten angegeben, wenn Sie nicht weiterkommen.

Antworten auf Hausaufgaben

Reduzieren Sie ein einfaches verschachteltes Array:

sourceArray = [
    [1,2,3],
    [4,5,6],
    [7,8,9]
];
flattenedSource = sourceArray.reduce((memo, val) => memo.concat(...val), []);

Schreiben Sie Ihre eigene Reduce-Funktion:

demoArray = [1,2,3,4,5];

// we accept an anonymous function, and an optional 'initial memo' value.
demoArray.my_reduce = function(reducer, seedMemo) {
    // if we did not pass in a second argument, then our first memo value 
    // will be whatever is in index zero. (Otherwise, it will 
    // be that second argument.)
    const initialMemoIsIndexZero = arguments.length < 2;

    // here we use that logic to set the memo value accordingly.
    let memo = initialMemoIsIndexZero ? this[0] : seedMemo;

    // here we use that same boolean to decide whether the first
    // value we pass in as iteratee is either the first or second
    // element
    const initialIteratee = initialMemoIsIndexZero ? 1 : 0;

    for (var i = initialIteratee; i < this.length; i++) {
        // memo is either the argument passed in above, or the 
        // first item in the list. initialIteratee is either the
        // first item in the list, or the second item in the list.
        memo = reducer(memo, this[i]);
    }

    // after we've compressed the array into a single value,
    // we return it.
    return memo;
};

Weniger ausführliche Version:

arr._reduce = function(reducer, seedMemo) {
    let memo = seedMemo || this[0];
    let i = seedMemo ? 0 : 1;

    for (;i < this.length; i++) {
        memo = reducer(memo, this[i]);
    }
    return memo;
};

(Denken Sie daran, hier keine Pfeilfunktion zu verwenden, damit Sie darauf zugreifen können!)

Die vollständige native Implementierung ermöglicht beispielsweise den Zugriff auf Dinge wie den Index, aber ich hoffe, dies hat Ihnen geholfen, ein unkompliziertes Gefühl für das Wesentliche zu bekommen. Auf MDN finden Sie die vollständige API.

Similar Posts

Leave a Reply

Your email address will not be published.